1. 程式人生 > >深入理解系列之JAVA多型機制(過載/重寫)

深入理解系列之JAVA多型機制(過載/重寫)

多型(Polymorphism)按字面的意思就是“多種狀態”。在面嚮物件語言中,介面的多種不同的實現方式即為多型(來自百度百科)。所以,按照這個意思其實多型的“主題”是物件,但是實際在我們運用中我們常把“過載”和“重寫”稱之為“多型”其實這是不嚴謹的!過載和重寫只是多型的存在帶來的兩種應用表現形式,也就是說正是因為過載和重寫我們才看到了多型的“威力”。所以,當我們談論多型實現機制的時候其實就是在談過載和重寫的實現機制罷了!

問題一、多型的好處是什麼?

多型是面向介面程式設計的實現基礎,而面向介面程式設計可以降低程式碼之間的耦合度,這樣說來比較抽象我們還是通過一個例子來說明:

package Duotai;

public
interface Living_beings { public void run(); } class Human implements Living_beings{ @Override public void run() { System.out.println("Human running"); } } class Dog implements Living_beings{ @Override public void run(){ System.out.println("Dog running"); } }
package Duotai;

public
class Main { public void call(Living_beings living_beings){ living_beings.run(); } public static void main(String[] args) { new Main().call(new Human()); new Main().call(new Dog()); } }

輸出:
Human running
Dog running

這就是面向介面程式設計,也就是說我在應用某個類的時候(call方法就是在應用某個類)並不是直接面向這個類而是面向這個介面——即傳遞這個介面變數,由於JAVA的多型特性,一個介面可以對應不同的“態”,這樣我就可以在後續動態的使用某個“態”(如main方法中使用Human這個“態”)和動態的新增這個“態”(如果有新的“生物”加入如cat,則直接實現Living_being並不需要變更call方法,因為面向的是介面所以只要介面需求不變call方法就不需要改變),而這一點正是“重寫”

的體現!

對於過載,允許同一個方法名具有不同的方法簽名,這樣呼叫該方法的時候回根據方法簽名的不同而呼叫我們需要的函式!我們可以在同一類中“過載”,當然也可以對父類的方法進行“過載”——對父類方法過載和重寫的不同在於重寫相當於覆蓋了父類的方法,而過載除了繼承了父類的該方法,又相當於添加了一個函式!過載使得程式碼更加易於區分同一個方法實現不同功能的特點!但是注意當過載遇到多型時,我們需要仔細分析:

package Duotai;

public class Main {

  public void call(Living_beings living_beings){
    living_beings.run();
  }

  public void run(Living_beings living_beings) {
    System.out.println("Now all is running");
  }

  public void run(Human human){
    System.out.println("Now human is running");
  }

  public void run(Dog dog){
    System.out.println("Now dog is running");
  }

  public static void main(String[] args) {
    Living_beings human = new Human();
    Living_beings dog = new Dog();
    new Main().run(human);
    new Main().run(dog);
  }
}

執行結果:
Now all is running
Now all is running

這裡run方法被過載(傳入不同的引數型別),但是當實際使用的時候,雖然實際的型別分別為Human和Dog,但是實際上方法呼叫的時候呼叫的是living_being!這裡就必須探討用於表現“多型”特性的“過載和重寫”在JVM中如何實現的問題了!

問題二、多型在JVM虛擬機器中的實現機制是什麼?

我們看到了對於重寫方法run(),方法呼叫(living_being.run())會根據物件的實際型別(即new出的型別)來確定最終的方法呼叫,但是到了方法過載則會根據宣告的型別來確定到底選擇哪個方法過載!之所以這樣,是因為涉及了JVM中的靜態分派和動態分派!

在講解靜態分派和動態分派之前,我們首先談論一下“方法呼叫”的概念!
“方法呼叫”就是為了解決在如何選擇到正確的目標方法的問題,方法呼叫分為“解析呼叫”和“分派呼叫”!在JVM中每個方法的資訊其實都以一種常量的形式存在於class檔案的常量池中,在類載入的解析階段會根據方法資訊解析並正確的載入到目標方法並執行,這其中有的資訊已經完全寫入到了class檔案中,僅從class檔案資訊就可解析出來,這種方法呼叫被稱為“解析”,也就是說解析一定是靜態的過程,在編譯期間就可以完全確定,在類裝載的階段就可以直接把涉及方法的符號引用轉變為可確定的直接引用!但是注意分派卻是既包含靜態的也包含動態的,靜態的概念和解析類似。動態是指編譯期在編譯時的確確定了一個符號引用,但是真正的直接引用要等到執行的時候才能確定!所以針對解析和分派有以下幾種指令:

invokestatic:呼叫靜態方法。
invokespecial:呼叫例項構造器<init>方法、私有方法和父類方法。

invokevirtual:呼叫所有的虛方法。
invokeinterface:呼叫介面方法,會在執行時再確定一個實現此介面的物件。
invokedynamic:先在執行時動態解析出呼叫點限定符所引用的方法,然後再執行該方法,在此之前的4條呼叫指令,分派邏輯是固化在Java虛擬機器內部的,而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的。

其中前兩種在解析階段就能被確定,我們稱之為“非虛方法”,如靜態方法、私有方法、例項構造器、父類方法。相反其他方法稱之為“虛方法”!這裡我們討論的是虛方法!

對於靜態分派,我們定義所有依賴靜態型別的定位方法的執行版本的分派動作,而典型的應用就是上文提到的過載:靜態分派發生在編譯階段,javac在編譯的時候根據靜態型別決定使用哪個版本,並把這個方法的符號引寫到main方法的兩條invokevirtual指令的引數中!
對於動態分派,實際上可以分為介面方法分派、繼承方法分派。JVM虛擬機器在方法分派前會為當前相關類(自身類、介面、父類、子類)生成一個方法表,對於繼承父類的方法分派:
JVM 首先檢視常量池宣告父類Parents的方法表,得出method方法在該方法表中的偏移量 offset,這就是該方法呼叫的直接引用。當解析出方法呼叫的直接引用後(方法表偏移量offset),JVM 執行真正的方法呼叫:根據例項方法呼叫的引數 this 得到具體的物件(即 繼承的某個物件child所指向的位於堆中的物件),據此得到該物件對應的方法表,進而呼叫方法表中的某個偏移量所指向的方法。

對於介面方法呼叫,其實現要簡單一些:
JVM 首先檢視常量池,確定方法呼叫的符號引用(名稱、返回值等等),然後利用 this 指向的例項得到該例項的方法表,進而搜尋方法表來找到合適的方法地址。因為每次介面呼叫都要搜尋方法表,所以從效率上來說,介面方法的呼叫總是慢於類方法的呼叫的,我們通過把上述“重寫”的例子反編譯成位元組碼指令來了解這個過程:
原始碼:

public static void main(String[] args) {
 Living_beings human = new Human();
    Living_beings dog = new Dog();
    human.run();
    dog.run();
  }

反編譯:

public static void main(java.lang.String[]);
    Code:
       0: new           #8                  // class Duotai/Human
       3: dup
       4: invokespecial #9                  // Method Duotai/Human."<init>":()V
       7: astore_1
       8: new           #10                 // class Duotai/Dog
      11: dup
      12: invokespecial #11                 // Method Duotai/Dog."<init>":()V
      15: astore_2
      16: aload_1
      17: invokeinterface #2,  1            // InterfaceMethod Duotai/Living_beings.run:()V
      22: aload_2
      23: invokeinterface #2,  1            // InterfaceMethod Duotai/Living_beings.run:()V
      28: return
}

如果你想知道更為詳細的方法表的資訊,請參考
java多型實現原理