• Java 中多态的实现(下)


    Java 中多态的另一个语法实现是重写。重载是通过静态分派实现的,重写则是通过动态分派实现的。

    在学习动态分派之前,需要对虚拟机的知识有一个初步的了解。

    虚拟机运行时数据区

    运行 Java 程序时,虚拟机先加载编译后的 .class 文件,然后根据文件内容来构建运行时数据区。关于 .class 文件的内容可以参考本书的第六章。

    runtime-data-area

    这里需要关心的是线程私有的虚拟机栈。每个线程都有自己的虚拟机栈,两者的生命周期相同。栈中的每个数据存储单元称为栈帧,栈帧里面存储的内容主要有:局部变量表、操作数栈、动态链接、方法出口。每个栈帧在虚拟机栈里面从入栈到出栈的过程,就对应每个方法从调用开始至执行完成的过程。栈顶表示当前栈帧,也就是正在执行的方法。

    局部变量表保存的是在方法中定义的局部变量和方法参数。

    操作数栈简称为操作栈,当方法开始执行时,操作栈是空的,然后将各种字节码指令写入或者取出操作栈,也就是出栈入栈操作,操作栈对应的是方法的具体执行。

    那么关于多态重写的部分,我们只关心方法的调用,即虚拟机是如何根据继承链上不同的对象来找到相应的方法呢?下面通过书中的示例进一步了解方法的调用。

    动态分派

    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();
            woman.sayHello();
            man = new Woman();
            man.sayHello();
        }
    }
    

    这段代码的的输出就不贴了,但是要了解方法的调用,可以简要的查看一下这段代码的字节码,了解关键部分的字节码指令即可。下面是使用javap -verbose DynamicDispatch命令输出的字节码中 main()方法的提取:

     public static void main(java.lang.String[]);
       descriptor: ([Ljava/lang/String;)V
       Code:
         stack=2, locals=3, args_size=1
            // 创建一个对象,并将其引用值压入栈顶,注意指的是操作栈
            // 这里是一个 Man 的实例
            0: new           #2                  // class jvmlearn/DynamicDispatch$Man
            // 复制栈顶数值并将复制值压入栈顶,上一步创建的对象的引用
            // 注意:因为下面的 invokespecial 指令会取出栈顶的创建的对象引用
            3: dup
            // 调用超类构造方法,实例初始化方法,私有方法
            // 也就是 Man 的父类 Human,这条暂时不用关心
            4: invokespecial #3                  // Method jvmlearn/DynamicDispatch$Man."<init>":()V
            // 将栈顶引用型数值存入第二个本地变量,可以往下翻,找到 LocalVariableTable
            // 第二行就是变量 man
            7: astore_1
            // 这里创建的是 Woman 的实例
            8: new           #4                  // class jvmlearn/DynamicDispatch$Woman
           // 同样将引用值压入栈顶
           11: dup
           // 同样调用父类的初始化方法
           12: invokespecial #5                  // Method jvmlearn/DynamicDispatch$Woman."<init>":()V
           // 将引用值存到局部变量表的第三个变量,也就是 woman
           15: astore_2
           // 将第二个引用类型本地变量推送至栈顶
           // 也就是 man
           16: aload_1
           // 调用实例方法
           // 这一步很关键,可以看到实际调用的是 man 的方法
           // 指令后面的 #6 表示常量池的第 6 项常量作为参数,后面的注释可以看到是 Human.sayHello() 方法的符号引用
           17: invokevirtual #6                  // Method jvmlearn/DynamicDispatch$Human.sayHello:()V
           // 将第三个引用类型本地变量推送至栈顶
           // 也就是 woman
           20: aload_2
           // 调用实例方法,也就是 woman 变量
           // 可以看到,两次调用方法的指令是一样的
           // 不同的地方只是实例不同
           21: invokevirtual #6                  // Method jvmlearn/DynamicDispatch$Human.sayHello:()V
           // 这一步又创建了一个 Woman 实例,将引用值压入栈顶
           24: new           #4                  // class jvmlearn/DynamicDispatch$Woman
           // 复制引用并压入栈顶
           27: dup
           // 调用父类的初始化方法
           28: invokespecial #5                  // Method jvmlearn/DynamicDispatch$Woman."<init>":()V
           // 将引用值存入第二个本地变量
           31: astore_1
           // 将引用类型的第二个本地推送至栈顶
           32: aload_1
           // 调用实例方法,可以看到和前面字节码行号为 17 的指令行,从指令到参数都一样
           // 而最终执行的目标方法却不一样
           33: invokevirtual #6                  // Method jvmlearn/DynamicDispatch$Human.sayHello:()V
           // 从当前方法返回 void
           36: return
         LineNumberTable:
         // 行号表,前面是源码行号,后面是字节码行号,可以看到上面的字节码指令的前面都有数字
           line 33: 0
           // eg. 源码的第 33 行,对应字节码的第 0 行
           line 34: 8
           line 35: 16
           line 36: 20
           line 37: 24
           line 38: 32
           line 39: 36
         LocalVariableTable:
         // 局部变量表
           Start  Length  Slot  Name   Signature
               0      37     0  args   [Ljava/lang/String;
               8      29     1   man   Ljvmlearn/DynamicDispatch$Human;
              16      21     2 woman   Ljvmlearn/DynamicDispatch$Human;
    

    调用实例方法的指令是invokevirtual,重写就是通过该指令实现的,该指令的多态查找过程如下:

    1. 找出操作栈顶元素所指向的对象的实际类型,记作 C;
    2. 如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常;
    3. 如果上一步没找到,则按照继承关系从下往上依次对 C 的各个父类进行第 2 步的操作;
    4. 如果最终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

    由于invokevirtual指令执行的第一步就是在运行器确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这就是 Java 中重写的本质。

    参考

  • 相关阅读:
    【网络攻击】之防止短信验证码接口被攻击
    【支付专区】之检查微信预下单返回结果
    【mybatis】之批量添加
    【java】之转码
    【springmvc】之常用的注解
    数字信号处理MATLAB简单序列
    matlab中同一文件定义子函数的方法
    MATLAB 单变量函数一阶及N阶求导
    Android 学习笔记1
    java socket tcp(服务器循环检测)
  • 原文地址:https://www.cnblogs.com/magexi/p/11822839.html
Copyright © 2020-2023  润新知