是父類或接口定義的引用變量可以指向子類或?qū)崿F(xiàn)類的實例對象腺晾,而程序調(diào)用的方法在運行期才動態(tài)綁定燕锥,就是引用變量所指向的具體實現(xiàn)對象的方法,也就是內(nèi)存里正在運行的那個對象的方法悯蝉,而不是引用變量的類型中定義的方法归形。
淺談多態(tài)機制的意義及實現(xiàn)
在面向?qū)ο缶幊蹋∣bject-Oriented Programming, OOP)中,多態(tài)機制無疑是其最具特色的功能鼻由,甚至可以說暇榴,不運用多態(tài)的編程不能稱之為OOP。這也是為什么有人說蕉世,使用面向?qū)ο笳Z言的編程和面向?qū)ο蟮木幊淌莾纱a事蔼紧。
多態(tài)并沒有一個嚴(yán)格的定義,維基百科上給它下的定義比較寬松:
Subtype polymorphism, almost universally called just polymorphism in the context of object-oriented programming, is the ability of one type, A, to appear as and be used like another type, B.
一讨彼、子類型和子類
這里我想先提一下子類型(Subtype)這個詞和子類(Subclass)的區(qū)別歉井,簡單地說柿祈,只要是A類運用了extends關(guān)鍵字實現(xiàn)了對B類的繼承哈误,那么我們就可以說Class A是Class B的子類,子類是一個語法層面上的詞躏嚎,只要滿足繼承的語法蜜自,就存在子類關(guān)系。
子類型比子類有更嚴(yán)格的要求卢佣,它不僅要求有繼承的語法重荠,同時要求如果存在子類對父類方法的改寫(override),那么改寫的內(nèi)容必須符合父類原本的語義虚茶,其被調(diào)用后的作用應(yīng)該和父類實現(xiàn)的效果方向一致戈鲁。
對二者的對比是想強調(diào)一點:只有保證子類都是子類型,多態(tài)才有意義嘹叫。
**二婆殿、多態(tài)的機制
**
本質(zhì)上多態(tài)分兩種:
編譯時多態(tài)(又稱靜態(tài)多態(tài))
運行時多態(tài)(又稱動態(tài)多態(tài))
重載(overload)就是編譯時多態(tài)的一個例子,編譯時多態(tài)在編譯時就已經(jīng)確定罩扇,運行時運行的時候調(diào)用的是確定的方法婆芦。
我們通常所說的多態(tài)指的都是運行時多態(tài)怕磨,也就是編譯時不確定究竟調(diào)用哪個具體方法,一直延遲到運行時才能確定消约。這也是為什么有時候多態(tài)方法又被稱為延遲方法的原因肠鲫。
在維基百科中多態(tài)的行為被描述為:
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.
下面簡要介紹一下運行時多態(tài)(以下簡稱多態(tài))的機制。
多態(tài)通常有兩種實現(xiàn)方法:
1.子類繼承父類(extends)
2.類實現(xiàn)接口(implements)
無論是哪種方法或粮,其核心之處就在于對父類方法的改寫或?qū)涌诜椒ǖ膶崿F(xiàn)导饲,以取得在運行時不同的執(zhí)行效果。
要使用多態(tài)被啼,在聲明對象時就應(yīng)該遵循一條法則:聲明的總是父類類型或接口類型帜消,創(chuàng)建的是實際類型。舉例來說浓体,假設(shè)我們要創(chuàng)建一個ArrayList對象泡挺,聲明就應(yīng)該采用這樣的語句:
List list =newArrayList();
而不是
ArrayList list =newArrayList();
在定義方法參數(shù)時也通常總是應(yīng)該優(yōu)先使用父類類型或接口類型命浴,例如某方法應(yīng)該寫成:
publicvoid doSomething(List list);
而不是
publicvoid doSomething(ArrayList list);
這樣聲明最大的好處在于結(jié)構(gòu)的靈活性:假如某一天我認(rèn)為ArrayList的特性無法滿足我的要求娄猫,我希望能夠用LinkedList來代替它,那么只需要在對象創(chuàng)建的地方把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).
虛擬機會在執(zhí)行程序時動態(tài)調(diào)用實際類的方法,它會通過一種名為動態(tài)綁定(又稱延遲綁定)的機制自動實現(xiàn)碍讯,這個過程對程序員來說是透明的悬蔽。
三、多態(tài)的用途
多態(tài)最大的用途我認(rèn)為在于對設(shè)計和架構(gòu)的復(fù)用捉兴,更進一步來說蝎困,《設(shè)計模式》中提倡的針對接口編程而不是針對實現(xiàn)編程就是充分利用多態(tài)的典型例子。定義功能和組件時定義接口倍啥,實現(xiàn)可以留到之后的流程中禾乘。同時一個接口可以有多個實現(xiàn),甚至于完全可以在一個設(shè)計中同時使用一個接口的多種實現(xiàn)(例如針對ArrayList和LinkedList不同的特性決定究竟采用哪種實現(xiàn))虽缕。
四始藕、多態(tài)的實現(xiàn)
下面從虛擬機運行時的角度來簡要介紹多態(tài)的實現(xiàn)原理,這里以Java虛擬機(Java Virtual Machine, JVM)規(guī)范的實現(xiàn)為例氮趋。
在JVM執(zhí)行Java字節(jié)碼時伍派,類型信息被存放在方法區(qū)中,通常為了優(yōu)化對象調(diào)用方法的速度剩胁,方法區(qū)的類型信息中增加一個指針诉植,該指針指向一張記錄該類方法入口的表(稱為方法表),表中的每一項都是指向相應(yīng)方法的指針摧冀。
方法表的構(gòu)造如下:
由于Java的單繼承機制倍踪,一個類只能繼承一個父類系宫,而所有的類又都繼承自O(shè)bject類。方法表中最先存放的是Object類的方法建车,接下來是該類的父類的方法扩借,最后是該類本身的方法。這里關(guān)鍵的地方在于缤至,如果子類改寫了父類的方法潮罪,那么子類和父類的那些同名方法共享一個方法表項,都被認(rèn)作是父類的方法领斥。
注意這里只有非私有的實例方法才會出現(xiàn)嫉到,并且靜態(tài)方法也不會出現(xiàn)在這里,原因很容易理解:靜態(tài)方法跟對象無關(guān)月洛,可以將方法地址直接引用何恶,而不像實例方法需要間接引用。
更深入地講嚼黔,靜態(tài)方法是由虛擬機指令invokestatic調(diào)用的细层,私有方法和構(gòu)造函數(shù)則是由invokespecial指令調(diào)用,只有被invokevirtual和invokeinterface指令調(diào)用的方法才會在方法表中出現(xiàn)唬涧。
由于以上方法的排列特性(Object——父類——子類)疫赎,使得方法表的偏移量總是固定的。例如碎节,對于任何類來說捧搞,其方法表中equals方法的偏移量總是一個定值,所有繼承某父類的子類的方法表中狮荔,其父類所定義的方法的偏移量也總是一個定值胎撇。
前面說過,方法表中的表項都是指向該類對應(yīng)方法的指針轴合,這里就開始了多態(tài)的實現(xiàn):
假設(shè)Class A是Class B的子類创坞,并且A改寫了B的方法method()碗短,那么在B的方法表中受葛,method方法的指針指向的就是B的method方法入口。
而對于A來說偎谁,它的方法表中的method方法則會指向其自身的method方法而非其父類的(這在類加載器載入該類時已經(jīng)保證总滩,同時JVM會保證總是能從對象引用指向正確的類型信息)。
結(jié)合方法指針偏移量是固定的以及指針總是指向?qū)嶋H類的方法域巡雨,我們不難發(fā)現(xiàn)多態(tài)的機制就在這里:
在調(diào)用方法時闰渔,實際上必須首先完成實例方法的符號引用解析,結(jié)果是該符號引用被解析為方法表的偏移量铐望。虛擬機通過對象引用得到方法區(qū)中類型信息的入口冈涧,查詢類的方法表茂附,當(dāng)將子類對象聲明為父類類型時,形式上調(diào)用的是父類方法督弓,此時虛擬機會從實際類的方法表(雖然聲明的是父類营曼,但是實際上這里的類型信息中存放的是子類的信息)中查找該方法名對應(yīng)的指針(這里用“查找”實際上是不合適的,前面提到過愚隧,方法的偏移量是固定的蒂阱,所以只需根據(jù)偏移量就能獲得指針),進而就能指向?qū)嶋H類的方法了狂塘。
我們的故事還沒有結(jié)束录煤,事實上上面的過程僅僅是利用繼承實現(xiàn)多態(tài)的內(nèi)部機制,多態(tài)的另外一種實現(xiàn)方式:實現(xiàn)接口相比而言就更加復(fù)雜荞胡,原因在于妈踊,Java的單繼承保證了類的線性關(guān)系,而接口可以同時實現(xiàn)多個泪漂,這樣光憑偏移量就很難準(zhǔn)確獲得方法的指針响委。所以在JVM中,多態(tài)的實例方法調(diào)用實際上有兩種指令:
- invokevirtual指令用于調(diào)用聲明為類的方法窖梁;
- invokeinterface指令用于調(diào)用聲明為接口的方法赘风。
當(dāng)使用invokeinterface指令調(diào)用方法時,就不能采用固定偏移量的辦法纵刘,只能老老實實挨個找了(當(dāng)然實際實現(xiàn)并不一定如此邀窃,JVM規(guī)范并沒有規(guī)定究竟如何實現(xiàn)這種查找,不同的JVM實現(xiàn)可以有不同的優(yōu)化算法來提高搜索效率)假哎。我們不難看出瞬捕,在性能上,調(diào)用接口引用的方法通扯婺ǎ總是比調(diào)用類的引用的方法要慢肪虎。這也告訴我們,在類和接口之間優(yōu)先選擇接口作為設(shè)計并不總是正確的惧蛹,當(dāng)然設(shè)計問題不在本文探討的范圍之內(nèi)扇救,但顯然具體問題具體分析仍然不失為更好的選擇。
個人見解:多態(tài)機制包括靜態(tài)多態(tài)(編譯時多態(tài))和動態(tài)多態(tài)(運行時多態(tài))香嗓,靜態(tài)多態(tài)比如說重載迅腔,動態(tài)多態(tài)是在編譯時不能確定調(diào)用哪個方法,得在運行時確定靠娱。動態(tài)多態(tài)的實現(xiàn)方法包括子類繼承父類和類實現(xiàn)接口沧烈。當(dāng)多個子類上轉(zhuǎn)型(不知道這么說對不)時,對象掉用的是相應(yīng)子類的方法像云,這種實現(xiàn)是與JVM有關(guān)的锌雀。
今天就分享這么多蚂夕,歡迎各位朋友在留言區(qū)評論,對于有價值的留言腋逆,我都會一一回復(fù)的双抽。如果覺得文章對你有一丟丟幫助,請給我點個贊吧闲礼,讓更多人看到該文章牍汹。
另外,小編最近將收集的Java程序員進階架構(gòu)師和面試的資料做了一些整理柬泽,免費分享給每一位學(xué)習(xí)Java的朋友慎菲,需要的可以進群:751827870,歡迎大家進群和我一起交流锨并。
本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布露该!