多型的機制原理

糊涂图-4060發表於2024-08-14

多型的機制原理

本質上多型分兩種

1、編譯時多型(又稱靜態多型)

2、執行時多型(又稱動態多型)

多型通常有兩種實現方法

1、子類繼承父類(extends)

2、類實現介面(implements)

要使用多型,在宣告物件時就應該遵循一條法則:宣告的總是父類型別或介面型別,建立的是實際型別。

例來說,假設我們要建立一個ArrayList物件,宣告就應該採用這樣的語句:

List list = new ArrayList();

而不是

ArrayList list = new ArrayList();

在定義方法引數時也通常總是應該優先使用父類型別或介面型別,例如某方法應該寫成:

public void doSomething(List list);

而不是

public void doSomething(ArrayList list);

這樣宣告最大的好處在於結構的靈活性:假如某一天我認為ArrayList的特性無法滿足我的要求,我希望能夠用LinkedList來代替它,那麼只需要在物件建立的地方把new ArrayList()改為new LinkedList即可,其它程式碼一概不用改動。

多型的實現

下面從虛擬機器執行時的角度來簡要介紹多型的實現原理,這裡以Java虛擬機器(Java Virtual Machine, JVM)規範的實現為例。

在JVM執行Java位元組碼時,型別資訊被存放在方法區中,通常為了最佳化物件呼叫方法的速度,方法區的型別資訊中增加一個指標,該指標指向一張記錄該類方法入口的表(稱為方法表),表中的每一項都是指向相應方法的指標。

方法表的構造如下:

由於Java的單繼承機制,一個類只能繼承一個父類,而所有的類又都繼承自Object類。方法表中最先存放的是Object類的方法,接下來是該類的父類的方法,最後是該類本身的方法。這裡關鍵的地方在於,如果子類改寫了父類的方法,那麼子類和父類的那些同名方法共享一個方法表項,都被認作是父類的方法。

注意這裡只有非私有的例項方法才會出現,並且靜態方法也不會出現在這裡,原因很容易理解:靜態方法跟物件無關,可以將方法地址直接引用,而不像例項方法需要間接引用。

更深入地講,靜態方法是由虛擬機器指令invokestatic呼叫的,私有方法和建構函式則是由invokespecial指令呼叫,只有被invokevirtual和invokeinterface指令呼叫的方法才會在方法表中出現。

由於以上方法的排列特性(Object——父類——子類),使得方法表的偏移量總是固定的。例如,對於任何類來說,其方法表中equals方法的偏移量總是一個定值,所有繼承某父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。

前面說過,方法表中的表項都是指向該類對應方法的指標,這裡就開始了多型的實現:

假設Class A是Class B的子類,並且A改寫了B的方法method(),那麼在B的方法表中,method方法的指標指向的就是B的method方法入口。

而對於A來說,它的方法表中的method方法則會指向其自身的method方法而非其父類的(這在類載入器載入該類時已經保證,同時JVM會保證總是能從物件引用指向正確的型別資訊)。

結合方法指標偏移量是固定的以及指標總是指向實際類的方法域,我們不難發現多型的機制就在這裡:

在呼叫方法時,實際上必須首先完成例項方法的符號引用解析,結果是該符號引用被解析為方法表的偏移量。虛擬機器透過物件引用得到方法區中型別資訊的入口,查詢類的方法表,當將子類物件宣告為父類型別時,形式上呼叫的是父類方法,此時虛擬機器會從實際類的方法表(雖然宣告的是父類,但是實際上這裡的型別資訊中存放的是子類的資訊)中查詢該方法名對應的指標(這裡用“查詢”實際上是不合適的,前面提到過,方法的偏移量是固定的,所以只需根據偏移量就能獲得指標),進而就能指向實際類的方法了。

我們的故事還沒有結束,事實上上面的過程僅僅是利用繼承實現多型的內部機制,多型的另外一種實現方式:實現介面相比而言就更加複雜,原因在於,Java的單繼承保證了類的線性關係,而介面可以同時實現多個,這樣光憑偏移量就很難準確獲得方法的指標。所以在JVM中,多型的例項方法呼叫實際上有兩種指令:

invokevirtual指令用於呼叫宣告為類的方法;

invokeinterface指令用於呼叫宣告為介面的方法。

當使用invokeinterface指令呼叫方法時,就不能採用固定偏移量的辦法,只能老老實實挨個找了(當然實際實現並不一定如此,JVM規範並沒有規定究竟如何實現這種查詢,不同的JVM實現可以有不同的最佳化演算法來提高搜尋效率)。我們不難看出,在效能上,呼叫介面引用的方法通常總是比呼叫類的引用的方法要慢。這也告訴我們,在類和介面之間優先選擇介面作為設計並不總是正確的,當然設計問題不在本文探討的範圍之內,但顯然具體問題具體分析仍然不失為更好的選擇。

相關文章