• Java 中多态的实现(上)


    Java 中语法上实现多态的方式分为两种:1. 重载、2. 重写,重载又称之为编译时的多态,重写则是运行时的多态。

    那么底层究竟时如何实现多态的呢,通过阅读『深入理解 Java 虚拟机』这本书(后文所指的书,如无特殊说明,指的都是这本书),对多态的实现过程有了一定的认识。以下内容是对学习内容的记录,以备今后回顾。

    写着写着突然发现内容有点多,分为上和下,上主要记录重载的知识点,下则是重写的相关知识点。

    重载

    重载就是根据方法的参数类型、参数个数、参数顺序的不同,来实现同名方法的不同调用,重载是通过静态分派来实现的,那么什么是静态分派呢,先展示一下书中的示例代码:

    public class StaticDispatch {
        static abstract class Human {
        }
    
        static class Man extends Human {
        }
    
        static class Woman extends Human {
        }
    
        public void sayHello(Human guy) {
            System.out.println("hello,guy!");
        }
    
        public void sayHello(Man guy) {
            System.out.println("hello,gentleman!");
        }
    
        public void sayHello(Woman guy) {
            System.out.println("hello,lady!");
        }
    
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            StaticDispatch sr = new StaticDispatch();
            sr.sayHello(man);
            sr.sayHello(woman);
        }
    }
    //输出:
    //hello,guy!
    //hello,guy!
    

    在 IDEA 中可以看到未被调用的方法名为灰色,这就可以知道示例代码在编译期间就已经确定了会调用的方法。在了解静态分派前,需要先熟悉一下静态类型和实际类型这两个概念。

    静态类型和实际类型

    Human man = new Man();
    

    Human 称为变量的静态类型(Static Type),或者叫做外观类型(Apparent Type),Man称为变量的实际类型(Actual Type)。

    书中有这样一段话:

    静态类型和实际类型在程序中都可以发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

    书中还举个例子:

    //实际类型变化
    Human man = new Man();
    man = new Woman();
    // 个人理解:man 的原本的实际类型是 Man,当第 3 行执行时,man 的实际类型就变成了 Woman
    //静态类型变化
    sr.sayHello((Man) man);
    // 个人理解:接着上一步,man 的静态类型是 Human,此时显式转换为 Man,作为 sayHello(Man guy)方法的参数
    sr.sayHello((Woman) man);
    // 个人理解:前面将 man 的静态类型转换为 Man,但是第 8 行方法中的 man 静态类型还是从 Human 转换成 Woman
    // 最终,man 的静态类型还是声明时的 Human
    

    对于书中的那段话,理解起来还是有点绕,以下是我的个人理解:

    1. 首先静态类型和实际类型都是针对变量而言的,描述的是变量的属性,并且这两个属性会发生变化;
    2. 静态类型指的是声明该变量时的类型,而实际类型指的是给该变量赋值时赋值号右边的变量类型;
    3. 静态类型的变化仅仅在使用时发生,这里要注意两点:1)仅仅的意思是要么变量的静态类型不变,要么就是在使用该变量的时候发生了变化;2)最终该变量的静态类型是不会改变的,还是原来声明时的类型。

    StaticDispatch类的 main 方法中,sayHello 方法的两次调用传入的参数静态类型是一致的,但是实际类型不通,结果调用的是同一个方法。从这一点可以看出,编译器是根据参数的静态类型来确定调用的方法的,静态类型在代码写完之后,就是已知的了,所以说重载在代码运行前就已经确定了。

    截取 main 方法的字节码:

      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=4, args_size=1
          // 0xbb 创建一个对象,并将其引用值压入栈顶
             0: new           #7                  // class jvmlearn/StaticDispatch$Man
             // 0x5c 复制栈顶数值并将复制值压入栈顶
             3: dup
             // 0xb7 调用超类构造方法,实例初始化方法,私有方法
             // 这个指令会用掉当前栈顶的值,所以前面复制了一份
             4: invokespecial #8                  // Method jvmlearn/StaticDispatch$Man."<init>":()V
             // 0x4c 将栈顶引用型数值存入第二个本地变量
             // 可以看下面的局部变量表
             7: astore_1
             8: new           #9                  // class jvmlearn/StaticDispatch$Woman
            11: dup
            12: invokespecial #10                 // Method jvmlearn/StaticDispatch$Woman."<init>":()V
            15: astore_2
            16: new           #11                 // class jvmlearn/StaticDispatch
            19: dup
            20: invokespecial #12                 // Method "<init>":()V
            23: astore_3
            // 0x2d 将第四个引用类型本地变量推送至栈顶
            24: aload_3
            25: aload_1
            // 0xb6 调用实例方法
            // 这里可以直接看到参数是 Human 类型,34 行的代码也一样
            26: invokevirtual #13                 // Method sayHello:(Ljvmlearn/StaticDispatch$Human;)V
            29: aload_3
            30: aload_2
            31: invokevirtual #13                 // Method sayHello:(Ljvmlearn/StaticDispatch$Human;)V
            34: return
          LineNumberTable:// 行号表
            line 30: 0
            line 31: 8
            line 32: 16
            line 33: 24
            line 34: 29
            line 35: 34
          LocalVariableTable:// 局部变量表,存了 main 方法的参数和局部变量,静态方法第一个局部变量不是 this,也没有 this
            Start  Length  Slot  Name   Signature
                0      35     0  args   [Ljava/lang/String;
                8      27     1   man   Ljvmlearn/StaticDispatch$Human;
               16      19     2 woman   Ljvmlearn/StaticDispatch$Human;
               24      11     3    sr   Ljvmlearn/StaticDispatch;
    

    现在回到静态分派的定义,所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用就是方法的重载。

    特点

    静态分派发生在编译阶段,是由编译器来确定使用哪个重载的方法,但这个重载的方法并不是唯一确定的。实际上编译器只是查找出当前重载的所有方法里面最合适的那一个。产生这种情况的原因,摘取书上的解释:

    字面量不需要定义,所以字面量没有显式的的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

    下面是书上给出的关于重载的这个特点的示例代码:

    public class Overload {
    
        public static void sayHello(Object arg) {
            System.out.println("hello Object");
        }
    
        public static void sayHello(int arg) {
            System.out.println("hello int");
        }
    
        public static void sayHello(long arg) {
            System.out.println("hello long");
        }
    
        public static void sayHello(Character arg) {
            System.out.println("hello Character");
        }
    
        public static void sayHello(char arg) {
            System.out.println("hello char");
        }
    
        public static void sayHello(char... arg) {
            System.out.println("hello char ...");
        }
    
        public static void sayHello(Serializable arg) {
            System.out.println("hello Serializable");
        }
    
        public static void main(String[] args) {
            sayHello('a');
        }
    }
    

    在 IDEA 中可以看到,调用的是 sayHello(char arg)方法。如果将该方法注释掉,编译器并不会报错,可以看到接下来调用的方法是 sayHello(int arg)。

    可以进一步测试,不断的注释当前调用的方法,就能发现编译器查找重载方法的规则,即自底向上的进行自动类型转换,自底向上进行查找。

    参考

    • 『深入理解 Java 虚拟机』:第二版,8.3.2 分派:1. 静态分派 P-247
  • 相关阅读:
    立方体
    file 图片预览
    [LeetCode][JavaScript]Single Number III
    [LeetCode][JavaScript]Longest Substring Without Repeating Characters
    [LeetCode][JavaScript]Missing Number
    [LeetCode][JavaScript]Course Schedule II
    [LeetCode][JavaScript]Course Schedule
    [LeetCode][JavaScript]Ugly Number II
    [LeetCode][JavaScript]Ugly Number
    [LeetCode][JavaScript]Single Number II
  • 原文地址:https://www.cnblogs.com/magexi/p/11815902.html
Copyright © 2020-2023  润新知