百萬紅包、火熱開啟!!!有你更精彩!
一、子型別和子類
這裡我想先提一下子型別(Subtype)這個詞和子類(Subclass)的區別,簡單地說,只要是A類運用了extends關鍵字實現了對B類的繼承,那麼我們就可以說Class A是Class B的子類,子類是一個語法層面上的詞,只要滿足繼承的語法,就存在子類關係。二、多型的機制
子型別比子類有更嚴格的要求,它不僅要求有繼承的語法,同時要求如果存在子類對父類方法的改寫(override),那麼改寫的內容必須符合父類原本的語義,其被呼叫後的作用應該和父類實現的效果方向一致。
對二者的對比是想強調一點:只有保證子類都是子型別,多型才有意義。
本質上多型分兩種:過載(overload)就是編譯時多型的一個例子,編譯時多型在編譯時就已經確定,執行時執行的時候呼叫的是確定的方法。
[quote]1、編譯時多型(又稱靜態多型)
2、執行時多型(又稱動態多型)
我們通常所說的多型指的都是執行時多型,也就是編譯時不確定究竟呼叫哪個具體方法,一直延遲到執行時才能確定。這也是為什麼有時候多型方法又被稱為延遲方法的原因。
在維基百科中多型的行為被描述為:
The primary usage of polymorphism in industry (object-oriented programming theory) is the ability of objects belonging to different types to respond to method, field, or property calls of the same name, each one according to an appropriate type-specific behavior.下面簡要介紹一下執行時多型(以下簡稱多型)的機制。
多型通常有兩種實現方法:
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即可,其它程式碼一概不用改動。
The programmer (and the program) does not have to know the exact type of the object in advance, and so the exact behavior is determined at run-time (this is called late binding or dynamic binding).虛擬機器會在執行程式時動態呼叫實際類的方法,它會通過一種名為動態繫結(又稱延遲繫結)的機制自動實現,這個過程對程式設計師來說是透明的。
[/quote]三、多型的用途
多型最大的用途我認為在於對設計和架構的複用,更進一步來說,《設計模式》中提倡的針對介面程式設計而不是針對實現程式設計就是充分利用多型的典型例子。定義功能和元件時定義介面,實現可以留到之後的流程中。同時一個介面可以有多個實現,甚至於完全可以在一個設計中同時使用一個介面的多種實現(例如針對ArrayList和LinkedList不同的特性決定究竟採用哪種實現)。四、多型的實現
下面從虛擬機器執行時的角度來簡要介紹多型的實現原理,這裡以Java虛擬機器(Java Virtual Machine, JVM)規範的實現為例。當使用invokeinterface指令呼叫方法時,就不能採用固定偏移量的辦法,只能老老實實挨個找了(當然實際實現並不一定如此,JVM規範並沒有規定究竟如何實現這種查詢,不同的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中,多型的例項方法呼叫實際上有兩種指令:
[quote]invokevirtual指令用於呼叫宣告為類的方法;
invokeinterface指令用於呼叫宣告為介面的方法。
[/quote]