本文主要是介绍【JVM补充】静态多分派,动态单分派,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
背景
之前在看过第二版的《深入理解JVM虚拟机》之后,对 java是一种静态多分派和动态单分派的语言 相关的说明和介绍,我理解的就不是很清晰,所以在前面的 JVM详解 一章中没有详细说明这一块内容,而关注到这个问题,最早还是在虚拟机栈中的 动态链接 内存分配的地方存在疑问,最近在看第三版的书,又看到了这个地方,所以在这里记录一下个人理解,以便之后回顾,如果有错误的地方,欢迎提出。
示例
package com.demo;public class OverLoadTest {static class Mobile{}static class Phone extends Mobile{}static class Pad extends Mobile{}static class TX{public void test(Mobile mobile){System.out.println("tx-mobile");}public void test(Phone phone){System.out.println("tx-phone");}public void test(Pad pad){System.out.println("tx-pad");}}static class QQ extends TX{@Overridepublic void test(Mobile mobile){System.out.println("qq-mobile");}@Overridepublic void test(Phone phone){System.out.println("qq-phone");}@Overridepublic void test(Pad pad){System.out.println("qq-pad");}}static class WeChat extends TX{@Overridepublic void test(Mobile mobile){System.out.println("wc-mobile");}@Overridepublic void test(Phone phone){System.out.println("wc-phone");}@Overridepublic void test(Pad pad){System.out.println("wc-pad");}}public static void main(String[] args) {TX qq = new QQ();TX wc = new WeChat();Mobile phone = new Phone();Mobile pad = new Pad();qq.test(phone);wc.test(pad);}}
打印输出结果:
qq-mobile
wc-mobile
看到这个结果,相信很多人都能猜到,但是为什么是这个结果,让我们来剖析一下,剖析完之后,大家就能明白其细节的原理了。
编译期间
首先,我们来看一下对当前类编译之后的结果:
命令:
javac OverLoadTest.java
javap -verbose OverLoadTest.class
Classfile /E:/jvm-demo/src/main/java/com/demo/OverLoadTest.classLast modified 2021-5-7; size 724 bytesMD5 checksum ac452de465b1d19e57ff6450d8660163Compiled from "OverLoadTest.java"
public class com.demo.OverLoadTestminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #12.#30 // java/lang/Object."<init>":()V#2 = Class #31 // com/demo/OverLoadTest$QQ#3 = Methodref #2.#30 // com/demo/OverLoadTest$QQ."<init>":()V#4 = Class #32 // com/demo/OverLoadTest$WeChat#5 = Methodref #4.#30 // com/demo/OverLoadTest$WeChat."<init>":()V#6 = Class #33 // com/demo/OverLoadTest$Phone#7 = Methodref #6.#30 // com/demo/OverLoadTest$Phone."<init>":()V#8 = Class #34 // com/demo/OverLoadTest$Pad#9 = Methodref #8.#30 // com/demo/OverLoadTest$Pad."<init>":()V#10 = Methodref #16.#35 // com/demo/OverLoadTest$TX.test:(Lcom/demo/OverLoadTest$Mobile;)V#11 = Class #36 // com/demo/OverLoadTest#12 = Class #37 // java/lang/Object#13 = Utf8 WeChat#14 = Utf8 InnerClasses#15 = Utf8 QQ#16 = Class #38 // com/demo/OverLoadTest$TX#17 = Utf8 TX#18 = Utf8 Pad#19 = Utf8 Phone#20 = Class #39 // com/demo/OverLoadTest$Mobile#21 = Utf8 Mobile#22 = Utf8 <init>#23 = Utf8 ()V#24 = Utf8 Code#25 = Utf8 LineNumberTable#26 = Utf8 main#27 = Utf8 ([Ljava/lang/String;)V#28 = Utf8 SourceFile#29 = Utf8 OverLoadTest.java#30 = NameAndType #22:#23 // "<init>":()V#31 = Utf8 com/demo/OverLoadTest$QQ#32 = Utf8 com/demo/OverLoadTest$WeChat#33 = Utf8 com/demo/OverLoadTest$Phone#34 = Utf8 com/demo/OverLoadTest$Pad#35 = NameAndType #40:#41 // test:(Lcom/demo/OverLoadTest$Mobile;)V#36 = Utf8 com/demo/OverLoadTest#37 = Utf8 java/lang/Object#38 = Utf8 com/demo/OverLoadTest$TX#39 = Utf8 com/demo/OverLoadTest$Mobile#40 = Utf8 test#41 = Utf8 (Lcom/demo/OverLoadTest$Mobile;)V
{public com.demo.OverLoadTest();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=5, args_size=10: new #2 // class com/demo/OverLoadTest$QQ3: dup4: invokespecial #3 // Method com/demo/OverLoadTest$QQ."<init>":()V7: astore_18: new #4 // class com/demo/OverLoadTest$WeChat11: dup12: invokespecial #5 // Method com/demo/OverLoadTest$WeChat."<init>":()V15: astore_216: new #6 // class com/demo/OverLoadTest$Phone19: dup20: invokespecial #7 // Method com/demo/OverLoadTest$Phone."<init>":()V23: astore_324: new #8 // class com/demo/OverLoadTest$Pad27: dup28: invokespecial #9 // Method com/demo/OverLoadTest$Pad."<init>":()V31: astore 433: aload_134: aload_335: invokevirtual #10 // Method com/demo/OverLoadTest$TX.test:(Lcom/demo/OverLoadTest$Mobile;)V38: aload_239: aload 441: invokevirtual #10 // Method com/demo/OverLoadTest$TX.test:(Lcom/demo/OverLoadTest$Mobile;)V44: returnLineNumberTable:line 59: 0line 60: 8line 62: 16line 63: 24line 65: 33line 66: 38line 67: 44
}
SourceFile: "OverLoadTest.java"
InnerClasses:static #13= #4 of #11; //WeChat=class com/demo/OverLoadTest$WeChat of class com/demo/OverLoadTeststatic #15= #2 of #11; //QQ=class com/demo/OverLoadTest$QQ of class com/demo/OverLoadTeststatic #17= #16 of #11; //TX=class com/demo/OverLoadTest$TX of class com/demo/OverLoadTeststatic #18= #8 of #11; //Pad=class com/demo/OverLoadTest$Pad of class com/demo/OverLoadTeststatic #19= #6 of #11; //Phone=class com/demo/OverLoadTest$Phone of class com/demo/OverLoadTeststatic #21= #20 of #11; //Mobile=class com/demo/OverLoadTest$Mobile of class com/demo/OverLoadTest
常量池和版本信息等我们就不关注了,我们主要看一下下面红框里的字节码,下面这两行编译后的指令对应的是java代码中的:
从上面我们能看出来,不管是方法的调用者,还是方法的参数,我们都选择的是子类的实例,但是编译之后的字节码能看出来,方法的入参和调用者都是父类,这是什么原因呢:
让我们先了解几个概念:
- 宗量:方法的入参和方法的调用者都是宗量
- 多分派:多于一个宗量的选择就是多分派
- 单分派:只针对一个宗量的选择就是单分派
由于在编译期间,编译器无法确定方法的最终调用者是哪一个子类,也无法确定方法的入参使用的是哪一个子类,所以都是选择的作为 静态变量 的父类进行方法的分派,也就是说,静态变量是 编译器可知,运行期不可变 的属性,但是可能有些同学还会有疑问,让我们改一下方式看看:
TX qq = new Random().nextInt() % 2 == 0 ? new QQ() : new WeChat();
从这刚代码,相信大家更能明白为什么编译期间选择静态变量的父类,而不是子类,因为只有在运行期间,才能确定创建的是哪一个子类实例。
所以从上面的例子能看出来,编译期间是针对方法的调用者和入参的 静态多分派 实现。
而最典型的例子就是方法的 重载 。
运行期间
从上面编译之后的字节码看到的是,方法的调用者和方法的入参都是父类,那为什么打印的结果不是tx.mobile呢,为什么方法的调用者都变成了子类呢,这是因为在代码运行期间,虚拟机能够确定方法的 最终调用者 是哪一个子类实例,所以 动态的分派 到对应子类 重写的方法 中去,所以在运行期间,针对方法的调用者这一宗量,JVM会采用 动态单分派 的方式进行方法的分派。
而典型的例子就是方法的 重写 。
综上,我们就能理解,为什么说 java是一门静态多分派,动态单分派的语言 了。
扩展
我们知道,在JVM 类加载期间 的 解析阶段 执行的是 符号引用转化为直接引用 的过程,在解析字节码文件的时候,会将字面量转换为真正引用的内存地址,而从上面的介绍我们又了解到,像方法的重写类似的逻辑,在这里是无法直接转换的,这里转换的只能是 非虚方法或者是非虚引用 也就是我们常说的静态方法,私有方法或者父类方法等,但是在该案例中,这两个方法的指令是: invokevirtual ,他表示的是调用的虚方法,所以在这里是无法直接转换的。
所以这就涉及到了虚拟机栈中的 动态链接 ,在运行期间,执行该栈帧中的代码,由于这是一个 虚方法的调用 ,所以在动态链接这里才真正的将方法的调用者分派到子类实例中去,实现子类方法重写的执行。
思考
之前在学习到动态链接的地方,由于不是很理解,回过头思考类加载时期的解析逻辑,再联系到编译时期的静态多分派。
由静态多分派,动态单分派,再一步步正推回动态链接。
果然,之前学习时跳过的坑,总是要回头填上的。
这篇关于【JVM补充】静态多分派,动态单分派的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!