理解java的多形性(轉)

BSDLite發表於2007-08-17
理解java的多形性(轉)[@more@]“對於物件導向的程式設計語言,多型性是第三種最基本的特徵(前兩種是資料抽象和繼承。”

“多形性”(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只是為圖形化程式設計設計的,這種認識當然是錯誤的。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-963312/,如需轉載,請註明出處,否則將追究法律責任。

相關文章