多態(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多態實現原理