标签:IV 实现 end 描述 rtu clip rac 顺序 并且
前几天突然被一个“家伙”问了几个问题,其中一个是:JAVA中的多态的实现原理是什么?
我一想,这肯定不是从语法的角度来阐释多态吧,隐隐约约地记得是与Class文件格式中的方法表有关,但是不知道虚拟机在执行的时候,是如何选择正确的方法来执行的了。so,趁着周末,把压箱底的《深入理解Java虚拟机》拿出来,重新看了下第6、7、8章中的内容,梳理一下:从我们用开发工具(Intellij 或者Eclipse)写的 .java 源程序,到经过javac 编译成class字节码文件,再到class字节码文件被加载到虚拟机并最终根据虚拟机指令执行选择出正确的(多态)方法执行的整个过程。
在讨论的多态(一般叫运行时多态)的时候,不可避免地要和重载(Overload)进行对比,为什么呢?因为这涉及到一种方法调用方式----分派(分派这个名字来源于 深入理解Java虚拟机 第8章8.3.2节)
重载(Overload)
重写(Override),或者叫运行时多态,这是本文主要要讨论的内容。
?
先来看看重载,(代码来源于书中)
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 sr = new StaticDispatch();
sr.sayHello(man);//hello, guy
sr.sayHello(woman);//hello, guy
}
}
再来看看重写(Override)
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();//man say hello
woman.sayHello();//woman say hello
}
}
在StaticDispatch.java 中,并不存在子类方法、父类方法。只有StaticDispatch.java的sayHello方法,即:sayHello 方法的接收者都是 StaticDispatch sr 对象,需要根据sayHello方法的参数类型来确定,具体执行下面这三个方法中的哪一个方法:
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");
}
而在DynamicDispatch.java中,首先有一个父类Human,它有一个sayHello方法,然后有两个子类:Woman、Man,它们分别@Override 了父类中的sayHello方法,也就是说:子类重写了父类中的方法。
上而就是从(源代码)语法的角度 描述了一下 重载(Overload) 和 重写(Override 或者叫运行时多态)的区别。程序要想执行,先要将源代码编译成字节码文件。
首先javac 命令将 StaticDispatch.java 和 DynamicDispatch.java编译成 class文件,然后使用分别使用下面命令输出这两个文件字节码的内容:
javap -verbose StaticDispatch
上面截取的是 StaticDispatch.java main方法中的方法表中的内容。方法表的结构 可参考书中第6.3.6小节的描述。
main方法中的代码,经过编译器编译成字节码指令后,存放在方法属性表集合中的一个名为 "Code" 的属性里面。
从上面的序号 26和 序号31 的字节码可以看出:sr.sayHello(man);
和 sr.sayHello(woman);
是由 invokevirtual指令执行的。
而且方法的符号引用都是:Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V
好,那咱就来看看,invokevirtual指令的具体执行过程,看它是如何将符号引用 解析到 具体的方法上的。
因为,覆盖(Override)或者说运行时多态也是通过invokevirtual指令来选择具体执行哪个方法的,因此:invokevirtual指令的解析过程 可以说是JAVA中实现多态的原理吧。
invokevirtual指令的解析过程大致分为以下几个步骤:
1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
因此,第一步,找到操作数栈顶的第一个元素所指的对象的实际类型,这个对象其实就是方法接收者的实际类型,它是StaticDispatch对象sr StaticDispatch sr = new StaticDispatch()
为什么是sr对象呢?比如对于序号26的invokevirtual指令,序号24、25行的两条aload_3 和 aload_1字节码指令 分别是把第四个引用类型的变量推送到栈顶,把第二个引用类型的变量推送到栈顶。而第四个引用类型的变量是StaticDispatch sr 对象;第二个引用类型的变量则是Man类的对象Human man = new Man()
第二步,根据常量 Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V
寻找 StaticDispatch类中哪个方法的简单名称和描述符都与该常量相同。
常量Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V
的简单名称是 ‘sayHello‘,描述符信息是:返回的类型为空,参数类型为Human,只有一个参数。
而在StaticDispatch.java中一共有三个不同的sayHello方法,它们的简单名称都是‘sayHello‘,而描述符中的参数类型为‘Human‘类型的方法是:
public void sayHello(Human guy) {
System.out.println("hello, guy");
}
因此,sr.sayHello(man);
实际调用的方法就是上面的public void sayHello(Human guy)
方法。
同样地,sr.sayHello(woman);
的方法接收者的实际类型是StaticDispatch对象sr,由序号31可知方法常量还是Method sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V
,因此,实际调用的方法还是public void sayHello(Human guy)
从这里可看出:对于重载(Overload)而言,它的方法接收者的类型是相同的,那调用哪个重载方法就取决于:传入的参数类型、参数的数量等。而参数类型在编译器生成字节码的时候就已经确定了,比如上面的sayHello方法的参数类型都是Human(sayHello:(Lorg/hapjin/dynamic/StaticDispatch$Human;)V
)
因此,sr.sayHello(man);
和sr.sayHello(woman);
执行的是相同的方法public void sayHello(Human guy){}
。
接下来看看:覆盖(Override),也即运行时多态的执行情况:
javap -verbose DynamicDispatch
上面截取的是DynamicDispatch.java的main方法的执行过程。从序号17和21 可知:man.sayHello();
和woman.sayHello();
也都是由虚拟机指令invokevirtual指令执行的,并且调用的sayHello方法的符号引用都是Method org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V
那为什么最终执行的结果却是:man.sayHello()
输出 ‘man say hello‘,而woman.sayHello()
输出‘woman say hello‘呢?
man.sayHello();//man say hello
woman.sayHello();//woman say hello
下面再来过一遍invokevirtual指令的执行过程。当虚拟机执行到man.sayHello()
这条语句时,invokevirtual指令第一步:找到操作数栈顶的第一个元素,这个元素就是序号7 astore_1存进去的,它是一个Man类型的对象。
接下来,第二步,在 Man 类中寻找与常量中描述符和简单名称都相符的方法,在这里常量是Method org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V
,而Man 类中与该常量的描述符和简单名称都相符的方法,显然就是 Man 类中的sayHello方法了。
于是invokevirtual指令就把 常量池中的类方法符号引用 解析 到了 具体的Man类的sayHello方法的直接引用上。
同理,类似地,在执行woman.sayHello()
这条语句时,invokevirtual指令找到的操作数栈顶的第一个元素是由 指令15astore_2存储进去的Woman类型的对象。于是,在Woman类中 寻找与常量池类方法的符号引用Method org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V
都相符的方法,这个方法就是Woman类中的sayHello方法。
从上面的invokevirtual指令的执行过程看,语句man.sayHello();
和woman.sayHello();
对应的类方法的符号引用是一样的,都是org/hapjin/dynamic/DynamicDispatch$Human.sayHello:()V
,但由于方法接受者的实际类型不同,一个是Man类型、另一个是Woman类型,因为最终执行的方法也就不一样了。
文中涉及到的一些额外的概念:
public void m(String a){}
,那简单名称就是 m以上纯个人理解,有些概念可能表述地不太严谨,若有错误,望指正,感激不尽。
写完这篇文章,我抬头望向窗外,天又黑了。目光缓缓移回到电脑屏幕上,一个技术人的追求到底是什么?我应该往哪个方向深入下去呢?后台、算法、ML、或者高大上的DL?
于是又想起了上一次的对话中那个人说的:关键是看你能不能持续地花时间把背后的原理搞清楚。
参考书籍:《深入理解JVM虚拟机》
原文:https://www.cnblogs.com/hapjin/p/9248525.html
标签:IV 实现 end 描述 rtu clip rac 顺序 并且
原文地址:https://www.cnblogs.com/hapjin/p/9248525.html