class Animal {
public void cry() {
System.out.println("xxx");
}
}
class Cat extends Animal {
public void cry() {
System.out.println("yyy");
}
public void bite() {
System.out.println("zzz");
}
}
public class PolymorphicTest {
public static void main(String ...args) {
Animal cat = new Cat();
cat.cry(); // yyy
((Cat)cat).bite(); // zzz
}
}
多態(tài)存在的三個(gè)必要條件:繼承、重寫、父類變量引用子類對(duì)象
數(shù)據(jù)轉(zhuǎn)型
- 向上轉(zhuǎn)型(up casting):就是將子類的對(duì)象轉(zhuǎn)型成父類的引用(就是父類引用指向了子類的對(duì)象,此時(shí)此引用只能訪問父類的成員)。
- 向下轉(zhuǎn)型(down casting):將向上轉(zhuǎn)型的引用再轉(zhuǎn)回來,就叫向下轉(zhuǎn)型(此時(shí)引用能訪問子類的成員變量)
多態(tài)中成員的特點(diǎn)
在多態(tài)中成員函數(shù)的特點(diǎn):動(dòng)態(tài)綁定
在編譯時(shí)期:參閱引用型變量所屬的類中是否有調(diào)用的方法。如果有,編譯通過,如果沒有編譯失敗
在運(yùn)行時(shí)期:參閱對(duì)象所屬的類中是否有調(diào)用的方法
簡(jiǎn)單總結(jié)就是:成員函數(shù)在多態(tài)調(diào)用時(shí),編譯看左邊,運(yùn)行看右邊在多態(tài)中,成員變量的特點(diǎn):靜態(tài)綁定
無論編譯和運(yùn)行,都參考左邊(引用型變量所屬的類)在多態(tài)中,靜態(tài)成員函數(shù)的特點(diǎn):靜態(tài)綁定
無論編譯和運(yùn)行,都參考左邊
因?yàn)殪o態(tài)只看類,沒有重寫,只有隱藏
多態(tài)好處
不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實(shí)現(xiàn)上,從而導(dǎo)致該引用調(diào)用的具體方法隨之改變,即不修改程序代碼就可以改變程序運(yùn)行時(shí)所綁定的具體代碼,讓程序可以選擇多個(gè)運(yùn)行狀態(tài),這就是多態(tài)性。多態(tài)性的實(shí)現(xiàn)主要通過動(dòng)態(tài)綁定
程序綁定
程序綁定是一個(gè)成員的調(diào)用與成員所在的類(成員主體)關(guān)聯(lián)起來。對(duì) Java 來說,綁定分為靜態(tài)綁定和動(dòng)態(tài)綁定
靜態(tài)綁定(前期綁定)
在編譯期已經(jīng)可以確定的信息,就在程序執(zhí)行前被綁定,此時(shí)由編譯器或其它連接程序?qū)崿F(xiàn)。針對(duì) Java 簡(jiǎn)單的可以理解為程序編譯期的綁定,綁定的是類信息
- Java 中的方法只有 final,static,private,重載方法和構(gòu)造方法是靜態(tài)綁定
- 所有的變量都是靜態(tài)綁定
動(dòng)態(tài)綁定(后期綁定)
編譯器在編譯階段不知道要調(diào)用哪個(gè)方法,在運(yùn)行時(shí)根據(jù)具體對(duì)象的類型進(jìn)行綁定,綁定的是對(duì)象信息
若一種語言實(shí)現(xiàn)了后期綁定,同時(shí)必須提供一些機(jī)制,可在運(yùn)行期間判斷對(duì)象的類型,并分別調(diào)用適當(dāng)?shù)姆椒?。也就是說,編譯器此時(shí)依然不知道對(duì)象的類型,但方法調(diào)用機(jī)制能自己去調(diào)查,找到正確的方法主體。不同的語言對(duì)后期綁定的實(shí)現(xiàn)方法是有所區(qū)別的。但我們至少可以這樣認(rèn)為:它們都要在對(duì)象中安插某些特殊類型的信息
重寫方法使用的是動(dòng)態(tài)綁定
方法調(diào)用
以 obj.func(param) 為例,隱式參數(shù) obj 聲明為 Cat 類的對(duì)象
- 編譯器查看對(duì)象的聲明類型和方法名
需要注意的是,有可能存在多個(gè)名字為 func 但參數(shù)簽名不一樣的方法。例如,可能存在方法 func(int) 和 func(String)。編譯器將會(huì)一一列舉所有 Cat 類中名為 func 的方法和其父類 Animal 中訪問屬性為 public 且名為 func 的方法(超類中的私有方法不可訪問)
這樣,編譯器就獲得了所有可能被調(diào)用的候選方法列表
接下來,編譯器將檢查調(diào)用方法時(shí)提供的參數(shù)簽名
如果在所有名為 func 的方法中存在一個(gè)與提供的參數(shù)簽名完全匹配的方法,那么就選擇這個(gè)方法。這個(gè)過程被稱為重載解析(overloading resolution)。例如,如果調(diào)用 func("hello"),編譯器會(huì)選擇 func(String),而不是 func(int)。由于自動(dòng)類型轉(zhuǎn)換的存在,例如 int 可以轉(zhuǎn)換為 double,Cat 可以轉(zhuǎn)換成 Animal 等,如果沒有找到與調(diào)用方法參數(shù)簽名匹配的方法,就進(jìn)行類型轉(zhuǎn)換后再繼續(xù)查找。如果編譯器沒有找到與參數(shù)類型匹配的方法,或者發(fā)現(xiàn)經(jīng)過類型轉(zhuǎn)換后有多個(gè)方法與之匹配,那么編譯錯(cuò)誤
這樣,編譯器就獲得了需要調(diào)用的方法名字和參數(shù)類型如果是 private、static、final,或者是構(gòu)造方法,那么編譯器將可以準(zhǔn)確地知道應(yīng)該調(diào)用哪個(gè)方法,這種調(diào)用方式稱為靜態(tài)綁定。與此對(duì)應(yīng)的是,調(diào)用的方法依賴于隱式參數(shù)的實(shí)際類型,這就需要在運(yùn)行時(shí)通過動(dòng)態(tài)綁定確定調(diào)用方法,比如上面的 cat.cry()
當(dāng)程序運(yùn)行,并且釆用動(dòng)態(tài)綁定調(diào)用方法時(shí),JVM 一定會(huì)調(diào)用與 obj 所引用對(duì)象的實(shí)際類型最合適的那個(gè)類的方法。我們已經(jīng)假設(shè) obj 的實(shí)際類型是 Cat,它是 Animal 的子類,如果 Cat 中定義了 func(String),就調(diào)用它,否則將在 Animal 類及其父類中尋找
方法表
每次調(diào)用方法都要進(jìn)行搜索,時(shí)間開銷相當(dāng)大,因此,JVM 預(yù)先為每個(gè)類創(chuàng)建了一個(gè)方法表,其中列出了所有方法的名稱、參數(shù)簽名和所屬的類。這樣一來,在真正調(diào)用方法的時(shí)候,JVM 僅查找這個(gè)表就行了。在上面的例子中,JVM 搜索 Cat 類的方法表,以便尋找與調(diào)用 func("hello") 相匹配的方法。這個(gè)方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果調(diào)用 super.func("hello"),編譯器將對(duì)父類的方法表進(jìn)行搜索
假設(shè) Animal 類包含 cry()、getName()、getAge() 三個(gè)方法,那么它的方法表如下:
cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
實(shí)際上,Animal 有默認(rèn)的父類 Object,會(huì)繼承 Object 的方法,所以上面列舉的方法并不完整
假設(shè) Cat 類覆蓋了 Animal 類中的 cry() 方法,并且新增了一個(gè)方法 climbTree(),那么它的參數(shù)列表為:
cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()
在運(yùn)行的時(shí)候,調(diào)用 obj.cry() 方法的過程如下:
- JVM 首先提取 obj 的實(shí)際類型的方法表,可能是 Animal 類的方法表,也可能是 Cat 類及其子類的方法表
- JVM 在方法表中搜索與 cry() 簽名匹配的方法,找到后,就知道它屬于哪個(gè)類了
- JVM 調(diào)用該方法
總結(jié)
- 先通過對(duì)象類型與方法名在方法表中找
- 通過參數(shù)簽名進(jìn)行重載解析
- 確認(rèn)屬于靜態(tài)綁定還是動(dòng)態(tài)綁定,最后調(diào)用