多態(tài)最大的作用就是為了傳參提供便利,但我們不應(yīng)該只看到這一層,還要往下再走走:為什么要用父類引用指向子類實(shí)例呢?
就好比你看到一把刀很鋒利,可以切菜,你不應(yīng)該疑惑“難道刀就是拿來切菜的嗎”,而應(yīng)該關(guān)注“為什么刀可以如此鋒利”...
回到你的問題上來,我們更應(yīng)該關(guān)心:為什么可以使用多態(tài)機(jī)制,以及為什么需要多態(tài)?
多態(tài)怎么實(shí)現(xiàn)的?
我并非計(jì)算機(jī)專業(yè),所以對于這個(gè)問題,只給出一個(gè)大概的解釋。多態(tài)從語法表面上看,就是子類對象可以賦值給父類引用,并且通過該引用可以動(dòng)態(tài)地調(diào)用不同子類的方法。
多態(tài)按實(shí)際用法又可以分為:
- 繼承多態(tài)
- 接口多態(tài)
所謂繼承多態(tài)(吸煙有害健康,我不抽煙):
class Son extends Father {
@Overrid
public void smoke() {
System.out.print("兒子抽煙");
}
}
class Daughter extends Father {
@Overrid
public void smoke() {
System.out.print("女兒抽煙");
}
}
// 繼承多態(tài),因?yàn)镾on、Daughter繼承了Father
Father obj = new Son();
obj.smoke(); // 打?。簝鹤映闊?obj = new Daughter();
obj.smoke(); // 打?。号畠撼闊?
所謂接口多態(tài):
class Son implements Swimmer {
@Overrid
public void swim() {
System.out.print("兒子游泳");
}
}
class Daughter implements Swimmer {
@Overrid
public void swim() {
System.out.print("兒子游泳");
}
}
// 接口多態(tài),因?yàn)镾on、Daughter實(shí)現(xiàn)了Swimmer
Swimmer obj = new Son();
obj.swim(); // 打?。簝鹤佑斡?obj = new Daughter();
obj.swim(); // 打?。号畠河斡?
實(shí)際開發(fā)接口多態(tài)更常用。
多態(tài)的實(shí)現(xiàn),依賴于2個(gè)大方面:
- 機(jī)制上的支持
- 編碼上的支持
機(jī)制支持
首先,編譯器要允許這種賦值方式,不然把son賦值給swimmer就會(huì)像把 int a賦值給String b一樣報(bào)錯(cuò)。
其次,運(yùn)行時(shí)要支持并且能通過某種機(jī)制找到真正的子類方法。
編碼支持
必須存在繼承(實(shí)現(xiàn))關(guān)系 + 子類必須重寫(實(shí)現(xiàn))父類的方法
我們一般所說的多態(tài),其實(shí)都是指方法的多態(tài)
什么意思呢?以上面Swimmer的代碼為例(假設(shè)整個(gè)工程只有這么幾個(gè)類),當(dāng)程序運(yùn)行時(shí),JVM中實(shí)際上并不存在一個(gè)對象叫Swimmer,自始至終只有Son和Daughter兩個(gè)對象,而且Son和Daughter都實(shí)現(xiàn)了Swimmer,且重寫了swim()方法。當(dāng)JVM運(yùn)行到:

JVM是怎么知道要打印“兒子游泳”的呢?換句話說,JVM怎么知道調(diào)用Son#swim()而不是Swimmer#swim()或者Daughter#swim()的呢?

這就涉及到所謂的“[虛方法]”和“虛方法表”了。JVM的知識點(diǎn)大概如下:

在類加載過程中,有l(wèi)oading、linking、initialization三個(gè)階段,其中l(wèi)inking(鏈接)階段又包括3個(gè)小階段:
- verify(驗(yàn)證)
- prepare(準(zhǔn)備)
- resolve(解析)
其中在resolve階段,JVM會(huì)針對類或接口、字段、類方法、接口方法等進(jìn)行相應(yīng)解析,其中方法信息會(huì)形成所謂的“虛方法表”。

也就是說,當(dāng)出現(xiàn)多態(tài)方法調(diào)用時(shí),底層會(huì)多一次“查表”的過程,也就是通過搜索虛方法表,確定本次實(shí)際應(yīng)該調(diào)用的方法(實(shí)際指向?qū)ο?實(shí)例對應(yīng)的類有無重寫父類方法),如果子類Override了父類方法,那么就會(huì)執(zhí)行子類方法。
多態(tài)與設(shè)計(jì)模式
很多初學(xué)編程的人,一定會(huì)記住兩句話,即使他們并不懂得其中含義:
- 面向?qū)ο蟮娜筇匦允牵悍庋b、繼承、多態(tài)
- 萬物皆對象
但在我眼里,封裝、繼承這倆貨和多態(tài)根本不是一個(gè)檔次的(就好比李云迪和郎朗),多態(tài)才是面向?qū)ο蟮暮诵暮透?,甚至沒有多態(tài)就沒有面向?qū)ο蟆?/strong>舉個(gè)例子,C語言沒有封裝嗎?不也是可以抽取方法嗎?也有結(jié)構(gòu)體呢,看起來不像對象嗎?再者,你問問自己,你使用繼承是為了什么?不就是為了貪圖父類的那一點(diǎn)點(diǎn)已經(jīng)寫好的方法,為了偷點(diǎn)懶嗎?既然是為了少寫一點(diǎn)代碼,我抽取成方法不行嗎?
所以,到底什么是面向?qū)ο竽兀?/p>
這就回到了我上面說的,多態(tài)才是面向?qū)ο蟮暮诵模ó?dāng)然,面向?qū)ο蟊举|(zhì)是一種編程思想的轉(zhuǎn)變)。當(dāng)我們有了多態(tài),才能寫出更加抽象的代碼,而抽象代表穩(wěn)定。
假設(shè)世界末日,外星人占領(lǐng)地球了,它們覺得必須殺雞儆猴,我們因?yàn)檎娴拇虿贿^,只能任由宰割。此時(shí)我們簽訂契約:你們可以殺一個(gè)動(dòng)物。于是我們送了一只實(shí)驗(yàn)室的小白鼠,因?yàn)樾“资笠彩莿?dòng)物呀。動(dòng)物這個(gè)詞是抽象的,后面我們送啥都可以,只要不送人。
再舉個(gè)編程的例子。假設(shè)在寫好的一個(gè)類文件中,你寫下這樣一段代碼:

如果后期接入拼多多,你就需要修改代碼。但如果使用策略模式,就可以用增量的方式代替修改(開閉原則):


具體可以參考:優(yōu)化代碼中大量的if/else,你有什么方案?
沒錯(cuò),這就是策略模式。而所謂的設(shè)計(jì)模式,其實(shí)有一本書的書名,恰恰點(diǎn)破了設(shè)計(jì)模式的本質(zhì):

是的,設(shè)計(jì)模式本質(zhì)是圍繞著“在面向?qū)ο蟮幕A(chǔ)上,如何復(fù)用設(shè)計(jì)”這個(gè)原則展開的...所以本質(zhì)又回到了面向?qū)ο蟆?/p>
為什么設(shè)計(jì)模式這么牛逼,能把很多看起來像“屎山一樣”的代碼優(yōu)化得清晰、簡潔?本質(zhì)上就是多態(tài)!
所謂“屎山一樣”的代碼,大概率就是因?yàn)楹笃谛枨蟛粩嗟?,開發(fā)人員在未經(jīng)思考的情況下肆意使用if else添加邏輯分支導(dǎo)致的!但分支是不會(huì)無緣無故消失的,只是借助設(shè)計(jì)模式把分支下推,最終交給了多態(tài)——JVM,你給我去查虛方法表。
換句話說就是:JVM,這坨屎你來吃。
最終,JVM帶著虛方法表承受了一切,而我們的上層代碼一掃陰霾,看起來干凈而整潔,也就是所謂的clean code...
所以,最后再問一句:多態(tài)真的就是用來傳參嗎?