9、静态分派和动态分派

1、基础知识点

1.1 分派

定义:确定执行哪个方法的过程
分类:静态分派 & 动态分派

a. 疑问

方法的执行不是取决于代码设置中的执行对象吗?为什么还要选择呢?

b. 回答

  • 若 一个对象对应于多个方法 时,就需要进行选择了
  • Java中的特性:多态,即重写 & 重载。

1.2 变量的静态类型 & 动态类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test { 

static abstract class Human {
}

static class Man extends Human {
}

static class Woman extends Human {
}

// 执行代码
public static void main(String[] args) {

Human man = new Man();
// 变量man的静态类型 = 引用类型 = Human:不会被改变、在编译器可知
// 变量man的动态类型 = 实例对象类型 = Man:会变化、在运行期才可知

}
}

变量的静态类型 = 引用类型 :不会被改变、在编译器可知

变量的动态类型 = 实例对象类型 :会变化、在运行期才可知

2、静态分派

定义:

  1. 根据变量的静态类型进行方法分派的行为即根据 变量的静态类型确定执行哪个方法
  2. 发生在编译期,所以不由 Java 虚拟机来执行

应用场景

方法重载(OverLoad) = 静态分派 = 根据 变量的静态类型确定执行(重载)哪个方法。

什么是重载?

如果我们想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同。这些方法之间的关系,我们称之为重载。重载的方法在编译过程中即可完成识别。具体到每一个方法调用,Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段:

  1. 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
  2. 如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
  3. 如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。

如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Test { 

// 类定义
static abstract class Human {
}

// 继承自抽象类Human
static class Man extends Human {
}

static class Woman extends Human {
}

// 可供重载的方法
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}

public void sayHello(Man guy) {
System.out.println("hello gentleman!");
}

public void sayHello(Woman guy) {
System.out.println("hello lady!");
}

// 测试代码
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test test = new Test();

test.sayHello(man); //man修饰的是Human
test.sayHello(woman); //woman修饰的是Human
}
}

// 运行结果
hello,guy!
hello,guy!

特别注意

a. 变量的静态类型发生变化的情况

可通过强制类型转换改变变量的静态类型

1
2
3
4
5
6
Human man = new Man(); 
test.sayHello((Man)man);
// 强制类型转换
// 此时man的静态类型从 Human 变为 Man

// 所以会调用sayHello()中参数为Man guy的方法,即sayHello(Man guy)

b. 静态分派的优先级匹配问题

问题描述:

背景:现需要进行静态分派

问题:程序中没有显示指 静态类型

解决方案:程序会根据静态类型的优先级从而选择优先的静态类型进行方法分配。

特别注意

  • 上面讲解的主要是基本数据类型的优先级匹配问题
  • 若是引用类型,则根据继承关系进行优先级匹配

注意只跟其编译时类型(即静态类型)相关

优先级顺序为:

  • char > int > long > float > double > Character > Serializable > Object >…
  • 其中…为变长参数,将其视为一个数组元素。变长参数的重载优先级最低。 因为 char 转型到 byte 或 short 的过程是不安全的 所以不会选择参数类型为byte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//比如:
public class Overload {

private static void sayHello(char arg){
System.out.println("hello char");
}

private static void sayHello(Object arg){
System.out.println("hello Object");
}

private static void sayHello(int arg){
System.out.println("hello int");
}

private static void sayHello(long arg){
System.out.println("hello long");
}

// 测试代码
public static void main(String[] args) {

sayHello('a');
}

}

// 运行结果
hello char

3、动态分派

3.1 定义

根据 变量的动态类型进行方法分派的行为

即根据 变量的动态类型确定执行哪个方法

3.2 应用场景

方法重写(Override)

如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。

对于 Java 语言中重写而 Java 虚拟机中非重写的情况,编译器会通过生成桥接方法 来实现 Java 中的重写语义。

由于对重载方法的区分在编译阶段已经完成,我们可以认为 Java 虚拟机不存在重载这一概念。

确切地说,Java 虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 定义类
class Human {
public void sayHello(){
System.out.println("Human say hello");

}
}

// 继承自 抽象类Human 并 重写sayHello()
class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");

}
}

class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");

}
}

// 测试代码
public static void main(String[] args) {

// 情况1
Human man = new man();
man.sayHello();

// 情况2
man = new Woman();
man.sayHello();
}
}

// 运行结果
man say hello
woman say hello

// 原因解析
// 1. 方法重写(Override) = 动态分派 = 根据 变量的动态类型 确定执行(重写)哪个方法
// 2. 对于情况1:根据变量(Man)的动态类型(man)确定调用man中的重写方法sayHello()
// 3. 对于情况2:根据变量(Man)的动态类型(woman)确定调用woman中的重写方法sayHello()

invokevirtual指令执行的第一步 = 确定接受者的实际类型

invokevirtual指令执行的第二步 = 将 常量池中 类方法符号引用 解析到不同的直接引用上

第二步即方法重写(Override)的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
例子:静态+动态
A ab = new B(); 这里ab的引用类型是A,但是它指向的内存是类型为B的一个实例。
ab.show(b)执行的时候:
1、首先编译期调用的方法都必须在 class A里面有的才行(因为你的引用类型为A)这里 class A有show(A obj) show(D obj)着两个方法。选择了show(A obj) (因为A是B的父类,向上转型)。这边是静态。
2、运行期ab.show(b)执行show(A obj)的适合,发现ab内存地址指向一个类型为B内存空间,如果class B Override 了 class A的show(A obj)方法,则调用B的方法而不是直接使用show(B obj),这边是动态。

class A {
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{
public String show(A obj){
return ("B and A");
}
public String show(B obj){
return ("B and B");
}
}

public class MultiTest{
public static void main(String[] args){
A ab = new B();
B b = new B();
C c = new C();
//ab的show方法先进去,找到参数为A obj,然后再看子类有没有重写 ,重写就执行子类的show
System.out.println(ab.show(b));
System.out.println(ab.show(c));
}
}

运行代码结果如下:
B and A
B and A
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
例子:动态+初始化编译

class Father{
int age = 40;
Father(){
show();
}
public void show () {
System.out.println(age);
}
}
class Son extends Father {
int age = 20;
public void show() {
System.out.println(age);
}
}
public class Demo {
public static void main(String[] args) {
Father f = new Son();
f.show();
}
}

//0 20
//先执行构造函数,父类构造函数执行show的时候
//父类的Age还没有初始化,默认为0,
//f.show的时候,先执行父类,但是子类重写过,所有就执行子类的show方法

public class Demo {
class Super{
int flag=1;
Super(){
test();
} void test(){
System.out.println("Super.test() flag="+flag);
}
}
class Sub extends Super{
Sub(int i){
flag=i;
System.out.println("Sub.Sub()flag="+flag);
} void test(){
System.out.println("Sub.test()flag="+flag);
}
}
public static void main(String[] args) {
new Demo().new Sub(5);
}
}
// Sub.Sub()flag= 1 Sub.Sub()flag= 5
//这个比上面那个区别是:这个子类没有申明flag,直接用父类的,上面的从新申明了

4、单分派与多分派

  • 方法的接收者、方法的参数都可以称为方法的宗量。根据分批基于多少种宗量,可以将分派划分为单分派和多分派。单分派是根据一个宗量对目标方法进行选择的,多分派是根据多于一个的宗量对目标方法进行选择的。

  • Java在进行静态分派时,选择目标方法要依据两点:一是变量的静态类型是哪个类型,二是方法参数是什么类型。因为要根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。

  • 运行时阶段的动态分派过程,由于编译器已经确定了目标方法的签名(包括方法参数),运行时虚拟机只需要确定方法的接收者的实际类型,就可以分派。因为是根据一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

5、虚拟机动态分派的实现

  • 由于动态分派是非常频繁的动作,而动态分派在方法版本选择过程中又需要在方法元数据中搜索合适的目标方法,虚拟机实现出于性能的考虑,通常不直接进行如此频繁的搜索,而是采用优化方法。

  • 其中一种“稳定优化”手段是:在类的方法区中建立一个虚方法表(Virtual Method Table, 也称vtable, 与此对应,也存在接口方法表——Interface Method Table,也称itable)。使用虚方法表索引来代替元数据查找以提高性能。其原理与C++的虚函数表类似。

  • 通过虚方法表存放各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口;如果子类中重写了这个方法,子类方法表中的地址将会诶替换为指向子类实现版本的入口地址。