分派

静态分派

  • 所有依赖静态类型来决定方法执行版本的分派动作,都成为静态分派。

  • 静态分配的最典型应用表现就是方法重载。

  • 静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

例子

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
public class Test {
static class Bird {
}

static class Sparrow extends Bird {
}

static class Magpie extends Bird {
}

public void talk(Bird bird) {
System.out.println("我是鸟!");
}

public void talk(Sparrow human) {
System.out.println("我是鸟中的麻雀!");
}

public void talk(Magpie human) {
System.out.println("我是鸟中的喜鹊!");
}

public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird magpie = new Magpie();
Test test = new Test();
test.talk(sparrow);
test.talk(magpie);
}
}

结果

我是鸟!
我是鸟!

分析

理解这个要明白重载是根据静态类型而不是实际类型来判断选择哪个方法的。

Bird sparrow = new Sparrow();

这个Bird 称为变量的 “静态类型”,而Sparrow 则是 变量的 “实际类型”。变量本身的静态类型是不会发生变化的,在编译期是可知的,上图也说明了这一点,程序还未运行便知道要调用的方法。实际类型变化的结果是在运行期间才可以确定的。编译器在编译程序的时候并不知道一个对象的实际类型是什么。

问题

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void String(Object param) {
System.out.println("object");
}

public static void test(int param) {
System.out.println("int");
}

public static void main(String[] args) {
Test.test('a');
}
}

这个代码输出什么?

分析

这说明了重载的版本并不是唯一的,但是往往必须确定一个“相对更加合适的”版本。

‘a’是char类型,就去寻找char类型的重载方法,如果此方法不存在,他就会自动进行类型转换’a’也可以代表数字97。所以他就会去执行int类型的重载方法了。

动态分派

  • 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

例子

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
public class Test {
static class Bird {
protected void talk() {
System.out.println("我是鸟!");
}
}

static class Sparrow extends Bird {
@Override
public void talk() {
System.out.println("我是鸟中的麻雀!");
}
}

static class Magpie extends Bird {
@Override
public void talk() {
System.out.println("我是鸟中的喜鹊!");
}
}

public static void main(String[] args) {
Bird sparrow = new Sparrow();
Bird magpie = new Magpie();
sparrow.talk();
magpie.talk();
sparrow = new Magpie();
sparrow.talk();
}
}

结果

我是鸟中的麻雀!
我是鸟中的喜鹊!
我是鸟中的喜鹊!

分析

显然重写不是根据静态类型确定调用哪个方法的,而是根据实际类型。子类重写了父类的方法不同的子类必然产生的动作不同,这肯定就不能依靠静态类型来确定了。

分析main()方法中的字节码:

invokevirtual

运行时解析过程

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记做C
  2. 如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstartMethodError异常。

这个说的很明白了,第1步不就找到实际类型吗!!这个过程就是Java语言中方法重写的本质。

既然这种多态性的根源是和invokevirtual这个指令有关,那么这个只能对方法生效对字段是无效的,因为字段并不使用这个指令,字段永远没有多态这个特性至少目前还没有(jdk14)

彻底理解多态

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
class Test {
public static void main(String[] args) {
System.out.println(new B().getValue());
}
static class A {
protected int value;
public A (int v) {
setValue(v);
}
public void setValue(int value) {
this.value= value;
}
public int getValue() {
try {
value ++;
return value;
} finally {
this.setValue(value);
System.out.println(value);
}
}
}
static class B extends A {
public B () {
super(5);
setValue(getValue()- 3);
}
public void setValue(int value) {
super.setValue(2 * value);
}
}
}

分析

查看输出结果 答案:22 34 17

解析:

执行对象实例化过程中遵循多态特性,调用的方法都是将要实例化的子类中的重写方法,只有明确调用了super.xxx关键词或者是子类中没有该方法时,才会去调用父类相同的同名方法。

  1. new B()构造一个B类的实例
  2. B的构造函数中super(5)中显示调用父类A的构造函数
  3. 执行A (int v) => setValue(v)
  4. 虽然构造函数是A类的构造函数,但此刻正在初始化的对象是B的一个实例,因此这里调用的实际是B类的setValue方法,于是调用B类中的setValue方法,而B类中setValue方法显示调用父类的setValue方法,将B实例的value值设置为2 x 5 = 10
  5. 至此super(5)这条语句执行完成,紧接着执行setValue(getValue() - 3)
  6. 由于B类中没有重写getValue方法,因此调用父类A的getValue方法。
  7. value++ 此时B的成员变量value=11,11这个返回值会先暂存起来,return value,跳过,先执行finally中的方法。
  8. this.setValue(value);调用的是B类的setValue方法,因为此刻正在初始化的是B类的一个对象(运行时多态),然后super.setValue(11 * 2)这里显示调用A类的setValue方法,将B的value设置为了22
  9. 然后System.out.println(value) 因此第一个打印的值为22。
  10. finally语句执行完毕,会把刚才暂存的11返回出去,也就是说这么经历了这些处理,getValue方法最终的返回值是11。
  11. setValue(11 - 3) => setValue(8)
  12. 执行setValue(8)执行的肯定是B类的setValue方法,然后value就变成了16。
  13. 到此new B()构造完毕
  14. 然后执行new B().getValue()方法,B中不存在此方法,所以调用的是A类的此方法。
  15. value++,B的成员变量value值为17,此时执行到return语句,先暂存,然后执行finally中语句,和之前原理一样,打印出34。
  16. 然后把value = 17返回出去,导致System.out.println(new B().getValue)就打印出17
  17. 所以最终的打印结果就是22 34 17