文章摘自:深入理解Java虚拟机 第二版 周志明著
-
静态分派
请看如下代码:
package com.gary.test.overload_overwrite;
/**
* 方法静态分派演示
* @author gary
*
*/
public class StaticDispatch {
static abstract class 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();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
sd.sayHello(woman);
}
}
运行结果为:
hello,guy!
hello,guy!
相信有Java编程经验的程序猿看到程序后都能得出正确的答案,但是为什么会选择执行参数类型为Human的重载呢?
我们把上述代码中Human称为变量的静态类型(Static Type),而Man则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,而区别是静态类型的变化仅仅是在使用时发生,变量本身静态类型不会被改变,并且最终的静态类型是在编译器可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
上述代码中两次sayhello方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中刻意定义 了两个静态类型相同而实际类型不同的变量。但是编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的,因此在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main方法里的两条invokevirtual指令的参数中。
所有依赖静态类型定位方法执行版本的分派动作称为静态分派。静态分派属于多分派。
静态分派的典型应用是方法重载。
静态分配派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
-
动态分派
请看如下代码:
package com.gary.test.overload_overwrite;
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
运行结果为:
man say hello
woman say hello
woman say hello
这个运行结果相信不会出乎任何人意料。但是问题来了:虚拟机如何知道要调用哪个方法的?
显然这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman,调用sayHello方法时候执行了不同行为。
原因很明显:这两个变量的实际类型不同。
Java虚拟机如何根据实际类型来分派方法执行版本的呢?我们使用javap命令输出这段代码的字节码,如下:
Compiled from "DynamicDispatch.java"
public class com.gary.test.overload_overwrite.DynamicDispatch {
public com.gary.test.overload_overwrite.DynamicDispatch();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class com/gary/test/overload_overwrite/DynamicDispatch$Man
3: dup
4: invokespecial #18 // Method com/gary/test/overload_overwrite/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #19 // class com/gary/test/overload_overwrite/DynamicDispatch$Woman
11: dup
12: invokespecial #21 // Method com/gary/test/overload_overwrite/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #22 // Method com/gary/test/overload_overwrite/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #22 // Method com/gary/test/overload_overwrite/DynamicDispatch$Human.sayHello:()V
24: new #19 // class com/gary/test/overload_overwrite/DynamicDispatch$Woman
27: dup
28: invokespecial #21 // Method com/gary/test/overload_overwrite/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #22 // Method com/gary/test/overload_overwrite/DynamicDispatch$Human.sayHello:()V
36: return
}
0-15是建立man和woman内存空间、调用Man和Woman类型的实例构造器,将这两个实例的引用存放在第1、2个局部变量表slot中。接下来16、20两句将刚才创建的两个对象的引用压到栈顶,这两个对象是将要执行sayHello方法的所有者,称接收者。17、21方法调用指令,而这两条调用指令是完全一样的(参数、指令都一样),但是最终执行的目标方法并不相同。
原因是invokevirtual指令的多态查找。
invokevirtual指令的运行时解析过程大致分为一下几个步骤:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相等的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过则返回IllegalAccessError异常。
3)否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。
这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。动态分派属于单分派。