Java:多型乃幸福本源
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對多型下了一個非常經典的結論,我們不妨大聲的朗讀幾遍:
多型就是指程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在編譯時並不確定,而是在程式執行期間才確定;即一個引用變數倒底會指向哪個類的例項物件,該引用變數發出的方法呼叫到底是哪個類中實現的方法,必須在由程式執行期間才能決定。因為在程式執行時才確定具體的類,這樣,不用修改源程式程式碼,就可以讓引用變數繫結到各種不同的類實現上,從而導致該引用呼叫的具體方法隨之改變,即不修改程式程式碼就可以改變程式執行時所繫結的具體程式碼,讓程式可以選擇多個執行狀態,這就是多型性。