深入理解系列之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多態實現原理

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