一個對象變量可以指示多種實際類型的現象稱為多態
允許不同類的對象對同一消息做出響應。方法的重載、類的覆蓋正體現了多態。
1、多態的機制
1.1 本質上多態分兩種
1、編譯時多態(又稱靜態多態) 2、運行時多態(又稱動態多態)
重載(overload 發生在一個類中,方法名必須相同,不同參數)就是編譯時多態的一個例子,編譯時多態在編譯時就已經確定,運行時運行的時候調用的是確定的方法。
我們通常所說的多態指的都是運行時多態,也就是編譯時不確定究竟調用哪個具體方法,一直延遲到運行時才能確定。這也是為什么有時候多態方法又被稱為延遲方法的原因。
下面簡要介紹一下運行時多態(以下簡稱多態)的機制。
1.2 多態通常有兩種實現方法
1、子類繼承父類(extends) 2、類實現接口(implements)
無論是哪種方法,其核心之處就在於對父類方法的改寫或對接口方法的實現,以取得在運行時不同的執行效果。
要使用多態,在聲明對象時就應該遵循一條法則:聲明的總是父類類型或接口類型,創建的是實際類型。
2、多態的實現原理
下面從虛擬機運行時的角度來簡要介紹多態的實現原理,這里以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實現可以有不同的優化算法來提高搜索效率)。我們不難看出,在性能上,調用接口引用的方法通常總是比調用類的引用的方法要慢。這也告訴我們,在類和接口之間優先選擇接口作為設計並不總是正確的。