從Java的重載/重寫往下看——Java分派

在聊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

我初次看到這段代碼的時候,怎麼也想不到這段代碼的結果會是如上結果。後來看了解析,發現這裏涉及到Java中的一個概念–分派

何爲分派

Java具有面向對象的三個基本特徵:封裝、繼承和多態,而分派,是多態性特徵(如“重載”、“重寫”)在虛擬機層面的一種體現。具體來說,分派是虛擬機確定正確的目標方法的一個過程。

靜態分派和動態分派

首先來了解一下靜態類型和實際類型這兩個概念

Map map = new HashMap<String, String>();

上例中的Map叫靜態類型/外觀類型,HashMap叫做實際類型。可以看出來,靜態類型一般爲抽象類/基類/接口,實際類型一般爲子類/實現類。
靜態類型和實際類型在程序中都可以發生一些變化, 區別是靜態類型的變化僅僅在使用時發生, 變量本身的靜態類型不會被改變, 並且最終的靜態類型是在編譯期可知的; 而實際類型變化的結果在運行期纔可確定, 編譯器在編譯程序的時候並不知道一個對象的實際類型是什麼。

  1. 靜態分派

所有依賴靜態類型來定位方法執行版本的分派動作稱爲靜態分派。

靜態分派的典型應用是方法重載。當在一個發生重載的方法中尋找最匹配的執行方法時,會優先根據靜態類型來選擇方法。現在再回頭看我們的引導案例,實際執行方法爲什麼是*sayHello(Human guy)*就一目瞭然了。

靜態分派發生在編譯階段,因此確定靜態分派的動作實際上不是由虛擬機來執行的。

另外,編譯器雖然能確定出方法的重載版本,但在很多情況下這個重載版本並不是“唯一的”,往往只能確定一個“更加合適的”版本,大家可以嘗試運行一下以下程序,依次註釋掉最匹配的結果,看看會打印出什麼結果。

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');
  }
}

  1. 動態分派

在運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。

我們來看一個動態分派的案例。

public class DynamicDispatch {
  static abstract class Human {
      protected abstract void sayHello();
  }

  static class Man extends Human {
      @Override
      protected void sayHello() {
          System.out.println("hello, man");
      }
  }

  static class Woman extends Human {
      @Override
      protected void sayHello() {
          System.out.println("hello, woman");
      }
  }

  public static void main(String[] args) {
      Human man = new Man();
      Human woman = new Woman();
      man.sayHello();
      woman.sayHello();
  }
}

相信稍微瞭解一點Java的人都能回答出來,上面代碼的運行結果是

hello, man
hello, woman

這是一個明顯的Java重寫方法案例,實際上,這個就是動態分派。只有在運行時,虛擬機才知道真正的執行主體是誰。
我們使用javap來分析一下上述代碼的彙編指令
動態分派的奧祕.png
可以看到,最終都是要執行虛方法

invokevirtual #6                  // Method cn/hewie/dispatch/dynamicdispatch/DynamicDispatch$Human.sayHello:()V

這裏的sayHello方法已經在編譯階段確定了,但是其執行的主體是從"aload_1"、"aload_2"這樣的變量中獲取的,這裏的值分別在上面的代碼中設置爲了Man和Women,所以產生這樣的運行結果。動態分派裏的“動態”就是這樣體現出來的,這也是重寫的在虛擬機層面的一種體現。

單分派和多分派

方法的接收者與方法的參數統稱爲方法的宗量。根據分派基於多少種宗量, 可以將分派劃分爲單分派和多分派兩種。 單分派是根據一個宗量對目標方法進行選擇, 多分派則是根據多個宗量對目標方法進行選擇。

總結

網上常說Java語言是一門靜態多分派、 動態單分派的語言,具體來說是因爲

  • Java中方法的執行要經歷兩個過程的選擇,一個是編譯期的靜態分派,一個是運行期的動態分派。這種分派場景出現的根本原因是多態。
  • 而一個方法需要確定的宗量有兩個,一個是方法的接受者(調用者),一個是方法的參數。
  • 進行編譯期的編譯時,需要確定的參數有兩個,一個是接受者,一個是參數,所以此時是多分派的。
  • 編譯之後,方法的參數就已經確定了。等進入到運行期,此時宗量就只剩下方法的接受者了,所以此時是單分派的。
  • 確定方法參數的時候,能夠影響到方法選擇的,就是方法的重載。
  • 確定方法接受者的時候,能夠影響到方法選擇的,就是方法的重寫。

參考文檔:
深入理解Java虛擬機:JVM高級特性與最佳實踐 周志明 著

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章