【JVM补充】静态多分派,动态单分派

2023-12-10 11:38

本文主要是介绍【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代码中的:


从上面我们能看出来,不管是方法的调用者,还是方法的参数,我们都选择的是子类的实例,但是编译之后的字节码能看出来,方法的入参和调用者都是父类,这是什么原因呢:
让我们先了解几个概念:

  1. 宗量:方法的入参和方法的调用者都是宗量
  2. 多分派:多于一个宗量的选择就是多分派
  3. 单分派:只针对一个宗量的选择就是单分派

由于在编译期间,编译器无法确定方法的最终调用者是哪一个子类,也无法确定方法的入参使用的是哪一个子类,所以都是选择的作为 静态变量 的父类进行方法的分派,也就是说,静态变量是 编译器可知,运行期不可变 的属性,但是可能有些同学还会有疑问,让我们改一下方式看看:

TX qq = new Random().nextInt() % 2 == 0 ? new QQ() : new WeChat();

从这刚代码,相信大家更能明白为什么编译期间选择静态变量的父类,而不是子类,因为只有在运行期间,才能确定创建的是哪一个子类实例。
所以从上面的例子能看出来,编译期间是针对方法的调用者和入参的 静态多分派 实现。
而最典型的例子就是方法的 重载

运行期间

从上面编译之后的字节码看到的是,方法的调用者和方法的入参都是父类,那为什么打印的结果不是tx.mobile呢,为什么方法的调用者都变成了子类呢,这是因为在代码运行期间,虚拟机能够确定方法的 最终调用者 是哪一个子类实例,所以 动态的分派 到对应子类 重写的方法 中去,所以在运行期间,针对方法的调用者这一宗量,JVM会采用 动态单分派 的方式进行方法的分派。
而典型的例子就是方法的 重写
综上,我们就能理解,为什么说 java是一门静态多分派,动态单分派的语言 了。

扩展

我们知道,在JVM 类加载期间解析阶段 执行的是 符号引用转化为直接引用 的过程,在解析字节码文件的时候,会将字面量转换为真正引用的内存地址,而从上面的介绍我们又了解到,像方法的重写类似的逻辑,在这里是无法直接转换的,这里转换的只能是 非虚方法或者是非虚引用 也就是我们常说的静态方法,私有方法或者父类方法等,但是在该案例中,这两个方法的指令是: invokevirtual ,他表示的是调用的虚方法,所以在这里是无法直接转换的。
所以这就涉及到了虚拟机栈中的 动态链接 ,在运行期间,执行该栈帧中的代码,由于这是一个 虚方法的调用 ,所以在动态链接这里才真正的将方法的调用者分派到子类实例中去,实现子类方法重写的执行。

思考

之前在学习到动态链接的地方,由于不是很理解,回过头思考类加载时期的解析逻辑,再联系到编译时期的静态多分派。
由静态多分派,动态单分派,再一步步正推回动态链接。
果然,之前学习时跳过的坑,总是要回头填上的。

这篇关于【JVM补充】静态多分派,动态单分派的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/477008

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2