【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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听