从虚拟机指令执行的角度分析JAVA中多态的实现原理

前几天突然被一个“家伙”问了几个问题,其中一个是: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
}
}

从语法的角度来聊一聊为什么上面的三个sayHello方法是重载的。方法之间是重载的要求这些方法具有:相同的简单名称 和 不同的特征签名。

  • 方法的简单名称是:没有类型和参数修饰的方法名称。比如上面的sayHello方法的简单名称就是 字符串"sayHello"
  • 方法的特征签名:可简单粗暴地理解成方法的 参数类型、参数顺序、参数个数。

    对于上面的三个sayHello方法而言,它们的简单名称是相同的,而参数类型不同(一个是Human 类型、一个是Woman类型、一个是Man类型),因此:它们是重载的。

    额外多补充一点:

    上面并没有提到方法的返回值,因为方法的返回值并不属于特征签名。

    当你在编辑器,比如IDEA或者Eclipse 中写了两个 简单名称相同、特征签名也相同、但是方法返回值不同的两个方法时,会报编译错误:“定义了两个同名的方法”。但是,这两个“同名的方法”是可以共存于同一个class文件中的。因为class文件格式规定了:描述符不完全一致的两个方法可以共存于同一个class文件中。

    那什么是方法的描述符呢?

    每个人编写代码的时候,给方法定义一个方法、给方法取个名字、带上参数……写出来的方法的无穷无尽的,如何用一套统一的规则来描述这些写出来的方法,就是方法描述符干的事情。

用描述符来描述方法时,先按照参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号内

从上面的描述符定义看,方法的描述符是包含了方法的返回值的。因此, 简单名称相同、特征签名也相同、但是方法返回值不同的两个方法可共存于同一个class文件中。

总结一下,讨论一个方法是否“相同”,这里涉及到了三个概念:

  • 简单名称
  • 方法特征签名
  • 方法描述符

我的理解,也许不准确:简单名称和特征签名在语法层面 来判断 编写的两个方法是否 是相同的;方法描述符在字节码层面 来判断 两个方法是否是 相同的;方法描述符不仅包含了特征签名、还包含了方法返回值。搞明白这三者的区别及作用就好了。

这个时候,你可能就有疑问了:既然简单名称相同、特征签名也相同、但是方法返回值不同的两个方法可共存于同一个class文件中,在jvm在执行代码(这两个方法)的时候怎么办呢?其实不用担心,在类加载的时候,有一个验证阶段,验证阶段包含了一个叫元数据验证的过程,元数据验证过程会验证 加载到内存方法区里面的class字节码是否符合方法重载的规则。因此,虽然这两个方法共存于同一个class文件中,但这种不符合java语义(语言规范)的情形 最终在验证阶段会被检查出来的。

再来看看重写(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" 的属性里面。

StaticDispatch 的main方法 字节码的执行过程

上面的 序号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类型,因为最终执行的方法也就不一样了。

文中涉及到的一些额外的概念:

  • 方法接收者:sr.sayHello(new Man()), sr 对象就是 sayHello方法的接收者
  • 常量:常量池中的常量,可参考常量池中的项目类型
  • 描述符:用来描述字段的数据类型,方法的参数列表和返回值,方法的参数列表指的是:方法有多少个参数、方法的参数是什么类型、参数的顺序
  • 简单名称:没有类型和参数修饰的方法或者字段名称。比如说方法:public void m(String a){},那简单名称就是 m

总之,jvm在判断具体执行哪个方法时,不仅要看方法的描述符(特征签名),而且要看方法的接收者的实际类型。而在多态中,从上面的示例中可以看出:方法的接收者的类型是不同的

以上纯个人理解,有些概念可能表述地不太严谨,若有错误,望指正,感激不尽。

写完这篇文章,我抬头望向窗外,天又黑了。目光缓缓移回到电脑屏幕上,一个技术人的追求到底是什么?我应该往哪个方向深入下去呢?后台、算法、ML、或者高大上的DL?

于是又想起了上一次的对话中那个人说的:关键是看你能不能持续地花时间把背后的原理搞清楚。

参考书籍:《深入理解JVM虚拟机》

原文:https://www.cnblogs.com/hapjin/p/9248525.html

从虚拟机指令执行的角度分析JAVA中多态的实现原理的更多相关文章

  1. 简单分析Java中审批业务流程业务原理

  2. 详细分析 Java 中实现多线程的方法有几种?(从本质上出发)

    详细分析 Java 中实现多线程的方法有几种?(从本质上出发) 正确的说法(从本质上出发) 实现多线程的官方正确方法: 2 种. Oracle 官网的文档说明 方法小结 方法一: 实现 Runnabl ...

  3. 详细分析 Java 中启动线程的正确和错误方式

    目录 启动线程的正确和错误方式 前文回顾 start 方法和 run 方法的比较 start 方法分析 start 方法的含义以及注意事项 start 方法源码分析 源码 源码中的流程 run 方法分 ...

  4. 分析Java中的length和length()

    在不适用任何带有自动补全功能的IDE的情况下,我们怎么获取一个数组的长度?如何获取字符串的长度? 这里我们先举用实例去分析一下:int[] arr=new int[3]:System.out.prin ...

  5. 深入Java核心 Java中多态的实现机制(1)

    在疯狂java中,多态是这样解释的: 多态:相同类型的变量,调用同一个方法时,呈现出多中不同的行为特征, 这就是多态. 加上下面的解释:(多态四小类:强制的,重载的,参数的和包含的) 同时, 还用人这 ...

  6. Java 中多态的实现(下)

    Java 中多态的另一个语法实现是重写.重载是通过静态分派实现的,重写则是通过动态分派实现的. 在学习动态分派之前,需要对虚拟机的知识有一个初步的了解. 虚拟机运行时数据区 运行 Java 程序时,虚 ...

  7. 个人对Java中多态的一些简单理解

    什么是多态 面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. 多态的定义:指允许不同类的对象对同一消息做出响应.即同一 ...

  8. Java中多态的一些简单理解

    什么是多态 .面向对象的三大特性:封装.继承.多态.从一定角度来看,封装和继承几乎都是为多态而准备的.这是我们最后一个概念,也是最重要的知识点. .多态的定义:指允许不同类的对象对同一消息做出响应.即 ...

  9. 关于java中多态的理解

    java三大特性:封装,继承,多态. 多态是java的非常重要的一个特性: 那么问题来了:什么是多态呢? 定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同的行 ...

随机推荐

  1. 【BZOJ5470】[FJOI2018]所罗门王的宝藏()

    [BZOJ5470][FJOI2018]所罗门王的宝藏() 题面 BZOJ 洛谷 有\(n+m\)个变量,给定\(k\)组限制,每次告诉你\(a_i+b_j=c_k\),问是否有可行解. 题解 一道很 ...

  2. HDU1269迷宫城堡(裸Tarjan有向图求强连通分量个数)

    迷宫城堡Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submiss ...

  3. 解决SSH连接出现 Software caused connection abort 的问题

    修改服务器中/etc/ssh/sshd.config 文件,将LoginGraceTime的值设为0,默认为2m,TCPKeepAlive 设为yes, 然后使用service sshd restar ...

  4. luogu2605 基站选址 (线段树优化dp)

    设f[i][j]表示在第i个村庄建第j个基站的花费 那么有$f[i][j]=min\{f[k][j-1]+w[k,i]\}$,其中w[k,i]表示在k,i建基站,k,i中间的不能被满足的村庄的赔偿金之 ...

  5. [WC2010]重建计划(分数规划+点分治+单调队列)

    题目大意:给定一棵树,求一条长度在L到R的一条路径,使得边权的平均值最大. 题解 树上路径最优化问题,不难想到点分治. 如果没有长度限制,我们可以套上01分数规划的模型,让所有边权减去mid,求一条路 ...

  6. [POI2006]OKR-Periods of Words(KMP)

    题意:给定一个字符串,求它的每个前缀的的一个最长前缀,使得它重复两边后能够覆盖原串. Solution 这题显然要在KMP的next数组上做一些手脚. 对于一个前缀,我们把它重复两遍,那么这个前缀的前 ...

  7. 【php】php目录路径函数系列

    在写框架和项目时候我们经常要获取绝对路径,php有内置函数realpath(),  也可以写个函数来实现这个功能 function getAbsolutePath($path) { $path = s ...

  8. mysql5.6做单向主从复制Replication

    原理场景:MySQL从3.23版本开始提供复制功能.指的是将主数据库的DDL和DML操作通过二进制日志传到从服务器(也叫从库),然后在从库上对这些日志重新执行, 从而使得从库和主库的数据保持同步. 优 ...

  9. bzoj1030 文本生成器

    题目链接 题意 给出\(n\)个字符串,要构造一个长度为\(m\)的字符串\(S\),使得给出的\(n\)个字符串中至少有一个是\(S\)的子串.问方案数. 思路 \(AC\)自动机+\(DP\) 考 ...

  10. java 分隔函数split("",-1)的用途

    转: java 分隔函数split("",-1)的用途 2017年12月14日 11:37:58 jaryle 阅读数:8517   1.如果字符串最后一位有值,则没有区别, 2. ...