【IT168 技術文檔】“對于面向對象的程序設計語言,多型性是第三種最基本的特征(前兩種是數據抽象和繼承。”
“多形性”(Polymorphism)從另一個角度將接口從具體的實施細節中分離出來,亦即實現了“是什么”與“怎樣做”兩個模塊的分離。利用多形性的概念,代碼的組織以及可讀性均能獲得改善。此外,還能創建“易于擴展”的程序。無論在項目的創建過程中,還是在需要加入新特性的時候,它們都可以方便地“成長”。
通過合并各種特征與行為,封裝技術可創建出新的數據類型。通過對具體實施細節的隱藏,可將接口與實施細節分離,使所有細節成為“private”(私有)。這種組織方式使那些有程序化編程背景人感覺頗為舒適。但多形性卻涉及對“類型”的分解。通過上一章的學習,大家已知道通過繼承可將一個對象當作它自己的類型或者它自己的基礎類型對待。這種能力是十分重要的,因為多個類型(從相同的基礎類型中衍生出來)可被當作同一種類型對待。而且只需一段代碼,即可對所有不同的類型進行同樣的處理。利用具有多形性的方法調用,一種類型可將自己與另一種相似的類型區分開,只要它們都是從相同的基礎類型中衍生出來的。這種區分是通過各種方法在行為上的差異實現的,可通過基礎類實現對那些方法的調用。
在這一章中,大家要由淺入深地學習有關多形性的問題(也叫作動態綁定、推遲綁定或者運行期綁定)。同時舉一些簡單的例子,其中所有無關的部分都已剝除,只保留與多形性有關的代碼。
7.1 上溯造型
在第6章,大家已知道可將一個對象作為它自己的類型使用,或者作為它的基礎類型的一個對象使用。取得一個對象句柄,并將其作為基礎類型句柄使用的行為就叫作“上溯造型”——因為繼承樹的畫法是基礎類位于最上方。
但這樣做也會遇到一個問題,如下例所示(若執行這個程序遇到麻煩,請參考第3章的3.1.2小節“賦值”):
//: 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()接收一個Instrument句柄,同時也接收從Instrument衍生出來的所有東西。當一個Wind句柄傳遞給tune()的時候,就會出現這種情況。此時沒有造型的必要。這樣做是可以接受的;Instrument里的接口必須存在于Wind中,因為Wind是從Instrument里繼承得到的。從Wind向Instrument的上溯造型可能“縮小”那個接口,但不可能把它變得比Instrument的完整接口還要小。
7.1.1 為什么要上溯造型
這個程序看起來也許顯得有些奇怪。為什么所有人都應該有意忘記一個對象的類型呢?進行上溯造型時,就可能產生這方面的疑惑。而且如果讓tune()簡單地取得一個Wind句柄,將其作為自己的自變量使用,似乎會更加簡單、直觀得多。但要注意:假如那樣做,就需為系統內Instrument的每種類型寫一個全新的tune()。假設按照前面的推論,加入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);
}
} ///:~
這樣做當然行得通,但卻存在一個極大的弊端:必須為每種新增的Instrument2類編寫與類緊密相關的方法。這意味著第一次就要求多得多的編程量。以后,假如想添加一個象tune()那樣的新方法或者為Instrument添加一個新類型,仍然需要進行大量編碼工作。此外,即使忘記對自己的某個方法進行過載設置,編譯器也不會提示任何錯誤。這樣一來,類型的整個操作過程就顯得極難管理,有失控的危險。
但假如只寫一個方法,將基礎類作為自變量或參數使用,而不是使用那些特定的衍生類,豈不是會簡單得多?也就是說,如果我們能不顧衍生類,只讓自己的代碼與基礎類打交道,那么省下的工作量將是難以估計的。
這正是“多形性”大顯身手的地方。然而,大多數程序員(特別是有程序化編程背景的)對于多形性的工作原理仍然顯得有些生疏。
7.2 深入理解
對于Music.java的困難性,可通過運行程序加以體會。輸出是Wind.play()。這當然是我們希望的輸出,但它看起來似乎并不愿按我們的希望行事。請觀察一下tune()方法:
public static void tune(Instrument i) {
// ...
i.play(Note.middleC);
}
它接收Instrument句柄。所以在這種情況下,編譯器怎樣才能知道Instrument句柄指向的是一個Wind,而不是一個Brass或Stringed呢?編譯器無從得知。為了深入了理解這個問題,我們有必要探討一下“綁定”這個主題。
7.2.1 方法調用的綁定
將一個方法調用同一個方法主體連接到一起就稱為“綁定”(Binding)。若在程序運行以前執行綁定(由編譯器和鏈接程序,如果有的話),就叫作“早期綁定”。大家以前或許從未聽說過這個術語,因為它在任何程序化語言里都是不可能的。C編譯器只有一種方法調用,那就是“早期綁定”。
上述程序最令人迷惑不解的地方全與早期綁定有關,因為在只有一個Instrument句柄的前提下,編譯器不知道具體該調用哪個方法。
解決的方法就是“后期綁定”,它意味著綁定在運行期間進行,以對象的類型為基礎。后期綁定也叫作“動態綁定”或“運行期綁定”。若一種語言實現了后期綁定,同時必須提供一些機制,可在運行期間判斷對象的類型,并分別調用適當的方法。也就是說,編譯器此時依然不知道對象的類型,但方法調用機制能自己去調查,找到正確的方法主體。不同的語言對后期綁定的實現方法是有所區別的。但我們至少可以這樣認為:它們都要在對象中安插某些特殊類型的信息。
Java中綁定的所有方法都采用后期綁定技術,除非一個方法已被聲明成final。這意味著我們通常不必決定是否應進行后期綁定——它是自動發生的。
為什么要把一個方法聲明成final呢?正如上一章指出的那樣,它能防止其他人覆蓋那個方法。但也許更重要的一點是,它可有效地“關閉”動態綁定,或者告訴編譯器不需要進行動態綁定。這樣一來,編譯器就可為final方法調用生成效率更高的代碼。
7.2.2 產生正確的行為
知道Java里綁定的所有方法都通過后期綁定具有多形性以后,就可以相應地編寫自己的代碼,令其與基礎類溝通。此時,所有的衍生類都保證能用相同的代碼正常地工作。或者換用另一種方法,我們可以“將一條消息發給一個對象,讓對象自行判斷要做什么事情。”
在面向對象的程序設計中,有一個經典的“形狀”例子。由于它很容易用可視化的形式表現出來,所以經常都用它說明問題。但很不幸的是,它可能誤導初學者認為OOP只是為圖形化編程設計的,這種認識當然是錯誤的。