第五章 品味Java子類型多態的魅力
"polymorphism(多態)"一詞來自希臘語,意為"多種形式"。多數Java程序員把多態看作對象的一種能力,使其能調用正確的方法版本。盡管如此,這種麵向實現的觀點導致了多態的神奇功能,勝於僅僅把多態看成純粹的概念。
Java中的多態總是子類型的多態。幾乎是機械式產生了一些多態的行為,使我們不去考慮其中涉及的類型問題。本文研究了一種麵向類型的對象觀點,分析了如何將對象能夠表現的行為和對象即將表現的行為分離開來。拋開Java中的多態都是來自繼承的概念,我們仍然可以感到,Java中的接口是一組沒有公共代碼的對象共享實現。
多態的分類
多態在麵向對象語言中是個很普遍的概念.雖然我們經常把多態混為一談,但實際上有四種不同類型的多態。在開始正式的子類型多態的細節討論前,然我們先來看看普通麵向對象中的多態。
Luca Cardelli和Peter Wegner("On Understanding Types, Data Abstraction, and Polymorphism"一文的作者, 文章參考資源鏈接)把多態分為兩大類----特定的和通用的----四小類:強製的,重載的,參數的和包含的。他們的結構如下:
在這樣一個體係中,多態表現出多種形式的能力。通用多態引用有相同結構類型的大量對象,他們有著共同的特征。特定的多態涉及的是小部分沒有相同特征的對象。四種多態可做以下描述:
強製的:一種隱式做類型轉換的方法。
重載的:將一個標誌符用作多個意義。
參數的:為不同類型的參數提供相同的操作。
包含的:類包含關係的抽象操作。
我將在講述子類型多態前簡單介紹一下這幾種多態。
強製的多態
強製多態隱式的將參數按某種方法,轉換成編譯器認為正確的類型以避免錯誤。在以下的表達式中,編譯器必須決定二元運算符'+'所應做的工作:
2.0 + 2.0
2.0 + 2
2.0 + "2"
第一個表達式將兩個double的操作數相加;Java中特別聲明了這種用法。
第二個表達式將double型和int相加。Java中沒有明確定義這種運算。不過,編譯器隱式的將第二個操作數轉換為double型,並作double型的加法。做對程序員來說十分方便,否則將會拋出一個編譯錯誤,或者強製程序員顯式的將int轉換為double。
第三個表達式將double與一個String相加。Java中同樣沒有定義這樣的操作。所以,編譯器將double轉換成String類型,並將他們做串聯。
強製多態也會發生在方法調用中。假設類Derived繼承了類Base,類C有一個方法,原型為m(Base),在下麵的代碼中,編譯器隱式的將Derived類的對象derived轉化為Base類的對象。這種隱式的轉換使m(Base)方法使用所有能轉換成Base類的所有參數。
C c = new C();
Derived derived = new Derived();
c.m( derived );
並且,隱式的強製轉換,可以避免類型轉換的麻煩,減少編譯錯誤。當然,編譯器仍然會優先驗證符合定義的對象類型。
重載的多態
重載允許用相同的運算符或方法,去表示截然不同的意義。'+'在上麵的程序中有兩個意思:兩個double型的數相加;兩個串相連。另外還有整型相加,長整型,等等。這些運算符的重載,依賴於編譯器根據上下文做出的選擇。以往的編譯器會把操作數隱式轉換為完全符合操作符的類型。雖然Java明確支持重載,但不支持用戶定義的操作符重載。
Java支持用戶定義的函數重載。一個類中可以有相同名字的方法,這些方法可以有不同的意義。這些重載的方法中,必須滿足參數數目不同,相同位置上的參數類型不同。這些不同可以幫助編譯器區分不同版本的方法。
編譯器以這種唯一表示的特征來表示不同的方法,比用名字表示更為有效。據此,所有的多態行為都能編譯通過。
強製和重載的多態都被分類為特定的多態,因為這些多態都是在特定的意義上的。這些被劃入多態的特性給程序員帶來了很大的方便。強製多態排除了麻煩的類型和編譯錯誤。重載多態像一塊糖,允許程序員用相同的名字表示不同的方法,很方便。
參數的多態
參數多態允許把許多類型抽象成單一的表示。例如,List抽象類中,描述了一組具有同樣特征的對象,提供了一個通用的模板。你可以通過指定一種類型以重用這個抽象類。這些參數可以是任何用戶定義的類型,大量的用戶可以使用這個抽象類,因此參數多態毫無疑問的成為最強大的多態。
乍一看,上麵抽象類好像是java.util.List的功能。然而,Java實際上並不支持真正的安全類型風格的參數多態,這也是java.util.List和java.util的其他集合類是用原始的java.lang.Object寫的原因(參考我的文章"A Primordial Interface?" 以獲得更多細節)。Java的單根繼承方式解決了部分問題,但沒有發揮出參數多態的全部功能。Eric Allen有一篇精彩的文章"Behold the Power of Parametric Polymorphism",描述了Java通用類型的需求,並建議給Sun的Java規格需求#000014號文檔"Add Generic Types to the Java Programming Language."(參考資源鏈接)
包含的多態
包含多態通過值的類型和集合的包含關係實現了多態的行為.在包括Java在內的眾多麵向對象語言中,包含關係是子類型的。所以,Java的包含多態是子類型的多態。
在早期,Java開發者們所提及的多態就特指子類型的多態。通過一種麵向類型的觀點,我們可以看到子類型多態的強大功能。以下的文章中我們將仔細探討這個問題。為簡明起見,下文中的多態均指包含多態。
麵向類型觀點
圖1的UML類圖給出了類和類型的簡單繼承關係,以便於解釋多態機製。模型中包含5種類型,4個類和一個接口。雖然UML中稱為類圖,我把它看成類型圖。如"Thanks Type and Gentle Class," 一文中所述,每個類和接口都是一種用戶定義的類型。按獨立實現的觀點(如麵向類型的觀點),下圖中的每個矩形代表一種類型。從實現方法看,四種類型運用了類的結構,一種運用了接口的結構。
圖1:示範代碼的UML類圖
以下的代碼實現了每個用戶定義的數據類型,我把實現寫得很簡單。
/* Base.java */
public class Base
{
public String m1()
{
return "Base.m1()";
}
public String m2( String s )
{
return "Base.m2( " + s + " )";
}
}
/* IType.java */
interface IType
{
String m2( String s );
String m3();
}
/* Derived.java */
public class Derived
extends Base
implements IType
{
public String m1()
{
return "Derived.m1()";
}