【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

相关文章

javax.net.ssl.SSLHandshakeException:异常原因及解决方案

《javax.net.ssl.SSLHandshakeException:异常原因及解决方案》javax.net.ssl.SSLHandshakeException是一个SSL握手异常,通常在建立SS... 目录报错原因在程序中绕过服务器的安全验证注意点最后多说一句报错原因一般出现这种问题是因为目标服务器

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.