在我刻板的印象里,西游記里的那段孫悟空和二郎神的精彩對戰就能很好的解釋“多態”這個詞:一個孫悟空,能七十二變;一個二郎神,也能七十二變;他們都可以變成不同的形態,但只需要悄悄地喊一聲“變”。 ...
01 多態是什麼
在我刻板的印象里,西游記里的那段孫悟空和二郎神的精彩對戰就能很好的解釋“多態”這個詞:一個孫悟空,能七十二變;一個二郎神,也能七十二變;他們都可以變成不同的形態,但只需要悄悄地喊一聲“變”。
Java的多態是什麼呢?其實就是一種能力——同一個行為具有不同的表現形式;換句話說就是,執行一段代碼,Java在運行時能根據對象的不同產生不同的結果。和孫悟空和二郎神都只需要喊一聲“變”,然後就變了,並且每次變得還不一樣;一個道理。
多態的前提條件有三個:
- 子類繼承父類
- 子類覆蓋父類的方法
- 父類引用指向子類對象
多態的一個簡單應用,來看程式清單1-1:
//子類繼承父類
public class Wangxiaoer extends Wanger {
public void write() { // 子類覆蓋父類方法
System.out.println("記住仇恨,表明我們要奮發圖強的心智");
}
public static void main(String[] args) {
// 父類引用指向子類對象
Wanger[] wangers = { new Wanger(), new Wangxiaoer() };
for (Wanger wanger : wangers) {
// 對象是王二的時候輸出:勿忘國恥
// 對象是王小二的時候輸出:記住仇恨,表明我們要奮發圖強的心智
wanger.write();
}
}
}
class Wanger {
public void write() {
System.out.println("勿忘國恥");
}
}
02 多態與後期綁定
現在,我們來思考一個問題:程式清單1-1在執行wanger.write()
時,由於編譯器只有一個Wanger引用,它怎麼知道究竟該調用父類Wanger的write()
方法,還是子類Wangxiaoer的write()
方法呢?
答案是在運行時根據對象的類型進行後期綁定,編譯器在編譯階段並不知道對象的類型,但是Java的方法調用機制能找到正確的方法體,然後執行出正確的結果。
多態機制提供的一個重要的好處程式具有良好的擴展性。來看程式清單2-1:
//子類繼承父類
public class Wangxiaoer extends Wanger {
public void write() { // 子類覆蓋父類方法
System.out.println("記住仇恨,表明我們要奮發圖強的心智");
}
public void eat() {
System.out.println("我不喜歡讀書,我就喜歡吃");
}
public static void main(String[] args) {
// 父類引用指向子類對象
Wanger[] wangers = { new Wanger(), new Wangxiaoer() };
for (Wanger wanger : wangers) {
// 對象是王二的時候輸出:勿忘國恥
// 對象是王小二的時候輸出:記住仇恨,表明我們要奮發圖強的心智
wanger.write();
}
}
}
class Wanger {
public void write() {
System.out.println("勿忘國恥");
}
public void read() {
System.out.println("每周讀一本好書");
}
}
在程式清單2-1中,我們在Wanger類中增加了read()方法,在Wangxiaoer類中增加了eat()方法,但這絲毫不會影響到write()方法的調用。write()方法忽略了周圍代碼發生的變化,依然正常運行。這讓我想起了金庸《倚天屠龍記》里九陽真經的口訣:“他強由他強,清風拂山崗;他橫由他橫,明月照大江。”
多態的這個優秀的特性,讓我們在修改代碼的時候不必過於緊張,因為多態是一項讓程式員“將改變的與未改變的分離開來”的重要特性。
03 多態與構造器
在構造器中調用多態方法,會產生一個奇妙的結果,我們來看程式清單3-1:
public class Wangxiaosan extends Wangsan {
private int age = 3;
public Wangxiaosan(int age) {
this.age = age;
System.out.println("王小三的年齡:" + this.age);
}
public void write() { // 子類覆蓋父類方法
System.out.println("我小三上幼兒園的年齡是:" + this.age);
}
public static void main(String[] args) {
new Wangxiaosan(4);
// 上幼兒園之前
// 我小三上幼兒園的年齡是:0
// 上幼兒園之後
// 王小三的年齡:4
}
}
class Wangsan {
Wangsan () {
System.out.println("上幼兒園之前");
write();
System.out.println("上幼兒園之後");
}
public void write() {
System.out.println("老子上幼兒園的年齡是3歲半");
}
}
從輸出結果上看,是不是有點詫異?明明在創建Wangxiaosan對象的時候,年齡傳遞的是4,但輸出結果既不是“老子上幼兒園的年齡是3歲半”,也不是“我小三上幼兒園的年齡是:4”。
為什麼?
因為在創建子類對象時,會先去調用父類的構造器,而父類構造器中又調用了被子類覆蓋的多態方法,由於父類並不清楚子類對象中的屬性值是什麼,於是把int類型的屬性暫時初始化為0,然後再調用子類的構造器(子類構造器知道王小二的年齡是4)。
04 多態與向下轉型
向下轉型是指將父類引用強轉為子類類型;這是不安全的,因為有的時候,父類引用指向的是父類對象,向下轉型就會拋出ClassCastException,表示類型轉換失敗;但如果父類引用指向的是子類對象,那麼向下轉型就是成功的。
來看程式清單4-1:
public class Wangxiaosi extends Wangsi {
public void write() {
System.out.println("記住仇恨,表明我們要奮發圖強的心智");
}
public void eat() {
System.out.println("我不喜歡讀書,我就喜歡吃");
}
public static void main(String[] args) {
Wangsi[] wangsis = { new Wangsi(), new Wangxiaosi() };
// wangsis[1]能夠向下轉型
((Wangxiaosi) wangsis[1]).write();
// wangsis[0]不能向下轉型
((Wangxiaosi)wangsis[0]).write();
}
}
class Wangsi {
public void write() {
System.out.println("勿忘國恥");
}
public void read() {
System.out.println("每周讀一本好書");
}
}
05 總結
我喜歡把複雜的事情儘量簡單化,把簡單的事情有趣化——多態是Java的三大特性之一,它本來需要長篇大論的介紹,但我覺得實在沒有必要,把關鍵的知識點提煉出來就足夠了。更重要的是,你要通過實踐去感知多態的優秀之處。
Java 技術驛站的chenssy對多態下了一個非常經典的結論,我們不妨大聲的朗讀幾遍:
多態就是指程式中定義的引用變數所指向的具體類型和通過該引用變數發出的方法調用在編譯時並不確定,而是在程式運行期間才確定;即一個引用變數倒底會指向哪個類的實例對象,該引用變數發出的方法調用到底是哪個類中實現的方法,必須在由程式運行期間才能決定。因為在程式運行時才確定具體的類,這樣,不用修改源程式代碼,就可以讓引用變數綁定到各種不同的類實現上,從而導致該引用調用的具體方法隨之改變,即不修改程式代碼就可以改變程式運行時所綁定的具體代碼,讓程式可以選擇多個運行狀態,這就是多態性。