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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM