【IT168 技術(shù)文檔】“對于面向?qū)ο蟮某绦蛟O(shè)計(jì)語言,多型性是第三種最基本的特征(前兩種是數(shù)據(jù)抽象和繼承。”
“多形性”(Polymorphism)從另一個(gè)角度將接口從具體的實(shí)施細(xì)節(jié)中分離出來,亦即實(shí)現(xiàn)了“是什么”與“怎樣做”兩個(gè)模塊的分離。利用多形性的概念,代碼的組織以及可讀性均能獲得改善。此外,還能創(chuàng)建“易于擴(kuò)展”的程序。無論在項(xiàng)目的創(chuàng)建過程中,還是在需要加入新特性的時(shí)候,它們都可以方便地“成長”。
通過合并各種特征與行為,封裝技術(shù)可創(chuàng)建出新的數(shù)據(jù)類型。通過對具體實(shí)施細(xì)節(jié)的隱藏,可將接口與實(shí)施細(xì)節(jié)分離,使所有細(xì)節(jié)成為“private”(私有)。這種組織方式使那些有程序化編程背景人感覺頗為舒適。但多形性卻涉及對“類型”的分解。通過上一章的學(xué)習(xí),大家已知道通過繼承可將一個(gè)對象當(dāng)作它自己的類型或者它自己的基礎(chǔ)類型對待。這種能力是十分重要的,因?yàn)槎鄠€(gè)類型(從相同的基礎(chǔ)類型中衍生出來)可被當(dāng)作同一種類型對待。而且只需一段代碼,即可對所有不同的類型進(jìn)行同樣的處理。利用具有多形性的方法調(diào)用,一種類型可將自己與另一種相似的類型區(qū)分開,只要它們都是從相同的基礎(chǔ)類型中衍生出來的。這種區(qū)分是通過各種方法在行為上的差異實(shí)現(xiàn)的,可通過基礎(chǔ)類實(shí)現(xiàn)對那些方法的調(diào)用。
在這一章中,大家要由淺入深地學(xué)習(xí)有關(guān)多形性的問題(也叫作動(dòng)態(tài)綁定、推遲綁定或者運(yùn)行期綁定)。同時(shí)舉一些簡單的例子,其中所有無關(guān)的部分都已剝除,只保留與多形性有關(guān)的代碼。
7.1 上溯造型
在第6章,大家已知道可將一個(gè)對象作為它自己的類型使用,或者作為它的基礎(chǔ)類型的一個(gè)對象使用。取得一個(gè)對象句柄,并將其作為基礎(chǔ)類型句柄使用的行為就叫作“上溯造型”——因?yàn)槔^承樹的畫法是基礎(chǔ)類位于最上方。
但這樣做也會遇到一個(gè)問題,如下例所示(若執(zhí)行這個(gè)程序遇到麻煩,請參考第3章的3.1.2小節(jié)“賦值”):
//: Music.java
// Inheritance & upcasting
package c07;
class Note {
private int value;
private Note(int val) { value = val; }
public static final Note
middleC = new Note(0),
cSharp = new Note(1),
cFlat = new Note(2);
} // Etc.
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument {
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play()");
}
}
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
} ///:~
其中,方法Music.tune()接收一個(gè)Instrument句柄,同時(shí)也接收從Instrument衍生出來的所有東西。當(dāng)一個(gè)Wind句柄傳遞給tune()的時(shí)候,就會出現(xiàn)這種情況。此時(shí)沒有造型的必要。這樣做是可以接受的;Instrument里的接口必須存在于Wind中,因?yàn)閃ind是從Instrument里繼承得到的。從Wind向Instrument的上溯造型可能“縮小”那個(gè)接口,但不可能把它變得比Instrument的完整接口還要小。
7.1.1 為什么要上溯造型
這個(gè)程序看起來也許顯得有些奇怪。為什么所有人都應(yīng)該有意忘記一個(gè)對象的類型呢?進(jìn)行上溯造型時(shí),就可能產(chǎn)生這方面的疑惑。而且如果讓tune()簡單地取得一個(gè)Wind句柄,將其作為自己的自變量使用,似乎會更加簡單、直觀得多。但要注意:假如那樣做,就需為系統(tǒng)內(nèi)Instrument的每種類型寫一個(gè)全新的tune()。假設(shè)按照前面的推論,加入Stringed(弦樂)和Brass(銅管)這兩種Instrument(樂器):
//: Music2.java
// Overloading instead of upcasting
class Note2 {
private int value;
private Note2(int val) { value = val; }
public static final Note2
middleC = new Note2(0),
cSharp = new Note2(1),
cFlat = new Note2(2);
} // Etc.
class Instrument2 {
public void play(Note2 n) {
System.out.println("Instrument2.play()");
}
}
class Wind2 extends Instrument2 {
public void play(Note2 n) {
System.out.println("Wind2.play()");
}
}
class Stringed2 extends Instrument2 {
public void play(Note2 n) {
System.out.println("Stringed2.play()");
}
}
class Brass2 extends Instrument2 {
public void play(Note2 n) {
System.out.println("Brass2.play()");
}
}
public class Music2 {
public static void tune(Wind2 i) {
i.play(Note2.middleC);
}
public static void tune(Stringed2 i) {
i.play(Note2.middleC);
}
public static void tune(Brass2 i) {
i.play(Note2.middleC);
}
public static void main(String[] args) {
Wind2 flute = new Wind2();
Stringed2 violin = new Stringed2();
Brass2 frenchHorn = new Brass2();
tune(flute); // No upcasting
tune(violin);
tune(frenchHorn);
}
} ///:~
這樣做當(dāng)然行得通,但卻存在一個(gè)極大的弊端:必須為每種新增的Instrument2類編寫與類緊密相關(guān)的方法。這意味著第一次就要求多得多的編程量。以后,假如想添加一個(gè)象tune()那樣的新方法或者為Instrument添加一個(gè)新類型,仍然需要進(jìn)行大量編碼工作。此外,即使忘記對自己的某個(gè)方法進(jìn)行過載設(shè)置,編譯器也不會提示任何錯(cuò)誤。這樣一來,類型的整個(gè)操作過程就顯得極難管理,有失控的危險(xiǎn)。
但假如只寫一個(gè)方法,將基礎(chǔ)類作為自變量或參數(shù)使用,而不是使用那些特定的衍生類,豈不是會簡單得多?也就是說,如果我們能不顧衍生類,只讓自己的代碼與基礎(chǔ)類打交道,那么省下的工作量將是難以估計(jì)的。
這正是“多形性”大顯身手的地方。然而,大多數(shù)程序員(特別是有程序化編程背景的)對于多形性的工作原理仍然顯得有些生疏。
7.2 深入理解
對于Music.java的困難性,可通過運(yùn)行程序加以體會。輸出是Wind.play()。這當(dāng)然是我們希望的輸出,但它看起來似乎并不愿按我們的希望行事。請觀察一下tune()方法:
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
它接收Instrument句柄。所以在這種情況下,編譯器怎樣才能知道Instrument句柄指向的是一個(gè)Wind,而不是一個(gè)Brass或Stringed呢?編譯器無從得知。為了深入了理解這個(gè)問題,我們有必要探討一下“綁定”這個(gè)主題。
7.2.1 方法調(diào)用的綁定
將一個(gè)方法調(diào)用同一個(gè)方法主體連接到一起就稱為“綁定”(Binding)。若在程序運(yùn)行以前執(zhí)行綁定(由編譯器和鏈接程序,如果有的話),就叫作“早期綁定”。大家以前或許從未聽說過這個(gè)術(shù)語,因?yàn)樗谌魏纬绦蚧Z言里都是不可能的。C編譯器只有一種方法調(diào)用,那就是“早期綁定”。
上述程序最令人迷惑不解的地方全與早期綁定有關(guān),因?yàn)樵谥挥幸粋€(gè)Instrument句柄的前提下,編譯器不知道具體該調(diào)用哪個(gè)方法。
解決的方法就是“后期綁定”,它意味著綁定在運(yùn)行期間進(jìn)行,以對象的類型為基礎(chǔ)。后期綁定也叫作“動(dòng)態(tài)綁定”或“運(yùn)行期綁定”。若一種語言實(shí)現(xiàn)了后期綁定,同時(shí)必須提供一些機(jī)制,可在運(yùn)行期間判斷對象的類型,并分別調(diào)用適當(dāng)?shù)姆椒āR簿褪钦f,編譯器此時(shí)依然不知道對象的類型,但方法調(diào)用機(jī)制能自己去調(diào)查,找到正確的方法主體。不同的語言對后期綁定的實(shí)現(xiàn)方法是有所區(qū)別的。但我們至少可以這樣認(rèn)為:它們都要在對象中安插某些特殊類型的信息。
Java中綁定的所有方法都采用后期綁定技術(shù),除非一個(gè)方法已被聲明成final。這意味著我們通常不必決定是否應(yīng)進(jìn)行后期綁定——它是自動(dòng)發(fā)生的。
為什么要把一個(gè)方法聲明成final呢?正如上一章指出的那樣,它能防止其他人覆蓋那個(gè)方法。但也許更重要的一點(diǎn)是,它可有效地“關(guān)閉”動(dòng)態(tài)綁定,或者告訴編譯器不需要進(jìn)行動(dòng)態(tài)綁定。這樣一來,編譯器就可為final方法調(diào)用生成效率更高的代碼。
7.2.2 產(chǎn)生正確的行為
知道Java里綁定的所有方法都通過后期綁定具有多形性以后,就可以相應(yīng)地編寫自己的代碼,令其與基礎(chǔ)類溝通。此時(shí),所有的衍生類都保證能用相同的代碼正常地工作。或者換用另一種方法,我們可以“將一條消息發(fā)給一個(gè)對象,讓對象自行判斷要做什么事情。”
在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,有一個(gè)經(jīng)典的“形狀”例子。由于它很容易用可視化的形式表現(xiàn)出來,所以經(jīng)常都用它說明問題。但很不幸的是,它可能誤導(dǎo)初學(xué)者認(rèn)為OOP只是為圖形化編程設(shè)計(jì)的,這種認(rèn)識當(dāng)然是錯(cuò)誤的。