系统性学习请点击jvm学习目录
本文详细的讲解了什么是单分派与多分派,以及为何静态分派是多分派而动态分派是单分派。这里需要提前有关于静态分派与动态分派的知识。
单分派&多分派 定义
首先来看一下书上给的定义。
方法的接收者(调用者)与方法的参数统称为方法的宗量。
单分派是根据一个宗量对目标方法进行选择(就是具体哪一个方法);而多分派则是根据多于一个宗量来对目标方法进行选择。
抓一下重点,单与多的区别在于一(多)个宗量,也就是一(多)个接收者或参数。看到这,也还是有点懵对吧,不急,我们来看例子。
举例说明
public class Test {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose QQ");
}
public void hardChoice(_360 arg) {
System.out.println("father choose _360");
}
public void hardChoice(Object arg) {
System.out.println("father choose obj");
}
}
public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choose QQ");
}
@Override
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
@Override
public void hardChoice(Object arg) {
System.out.println("son choose obj");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
Object qq = new QQ();
Object aa = new _360();
father.hardChoice(qq);
son.hardChoice(aa);
}
}
学习了静态分派,我们知道实际上字节码文件就是静态分派之后的结果,在编译期确定目标方法到底是哪一个方法。
那么用静态分派来分析下我们的代码。
qq与aa都是Object类型的变量,它们分别指向QQ实例对象与_360实例对象;而father与son都是Father类型的变量,它们分别指向Father实例对象与Son实例对象。所以在编译期,编译器要把代码编译成字节码文件,就得确定father.hardChoice(qq); son.hardChoice(aa);
这两个方法到底用的是哪两个方法(指令总不可能写或许是A方法,或许是B方法吧,总得有个定论)。由于编译期都是按照静态类型来确定的,这里father与son的静态类型都是Father类,而qq与aa的静态类型都是Object类,所以这里两个方法在字节码中将被选择为两条Father.hardChoice(Object arg)
方法。
下面我们用javap来看一下这个类的字节码指令解析。验证我们的分析结果。
Code:
0: new #2 // class Test$Father
3: dup
4: invokespecial #3 // Method Test$Father."<init>":()V
7: astore_1
8: new #4 // class Test$Son
11: dup
12: invokespecial #5 // Method Test$Son."<init>":()V
15: astore_2
16: new #6 // class Test$QQ
19: dup
20: invokespecial #7 // Method Test$QQ."<init>":()V
23: astore_3
24: new #8 // class Test$_360
27: dup
28: invokespecial #9 // Method Test$_360."<init>":()V
31: astore 4
33: aload_1
34: aload_3
35: invokevirtual #10 // Method Test$Father.hardChoice:(Ljava/lang/Object;)V
38: aload_2
39: aload 4
41: invokevirtual #10 // Method Test$Father.hardChoice:(Ljava/lang/Object;)V
44: return
可以看到35,41行指令,都是Method Test$Father.hardChoice:(Ljava/lang/Object;)V
这验证了我们的分析。而从上面的分析中,我们可以看到,在编译期选择方法版本(目标方法)时,静态分派在这里考虑了接收者(调用者)和参数这两个宗量。所以静态分派就是多分派。
下面继续来分析java代码在运行过程中的情况。
当java代码真正运行到father.hardChoice(qq); son.hardChoice(aa);
时,JVM会发现,原来father变量指向的是Father类型,son变量指向的是Son类型。此时动态分派只关心接收者,并不关心参数,所以只考虑一个宗量,便是单分派。
???这里你可能会问,为啥就不考虑参数啊,你就没发现qq和aa都不是Object类型吗?
下面来解释这个问题,其实在介绍动态分派时已经提到了答案。动态分派为什么能够在运行过程中发现接收者的实际类型呢?这两个方法在字节码中的指令不是一模一样吗?
答案就在于invokevirtual这条指令。在《java虚拟机规范》中对于该指令的运行时解析过程大致分为这几步:
-
找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
-
如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IlleagalAccessError异常。
-
否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
-
如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
在字节码文件中,我们看到,在invokevirtual指令之前都有一个将局部变量表的变量加载到栈顶。而把注意力放在上面四步的第一步上,我们看到该指令会找到操作数栈顶的元素指向的实际类型。连起来就是找到接收者的实际类型,在实际类型中寻找方法。
这就是动态分派的核心所在,所以能够根据实际类型来调用正确的方法,而同时,从上面四步我们也找不到对于参数实际类型的确定,所以可以这样理解,动态分派只考虑接收者,没考虑参数。(换句话说,就是在编译期,参数已经都决定为Object。
所以动态分派是单分派。
按照以上分析,最终的结果应该是Method Test$Father.hardChoice:(Ljava/lang/Object;)V
和Method Test$Son.hardChoice:(Ljava/lang/Object;)V
,也就是打印出father choose obj和son choose obj
下面是结果
可以看到,和我们的分析结果一致。
参考资料
- 周志明《深入理解JAVA虚拟机》
- https://blog.csdn.net/fan2012huan/article/details/51004615