第五章 品味Java子類型多態的魅力(2 / 3)

public String m3()

{

return "Derived.m3()";

}

}

/* Derived2.java */

public class Derived2

extends Derived

{

public String m2( String s )

{

return "Derived2.m2( " + s + " )";

}

public String m4()

{

return "Derived2.m4()";

}

}

/* Separate.java */

public class Separate

implements IType

{

public String m1()

{

return "Separate.m1()";

}

public String m2( String s )

{

return "Separate.m2( " + s + " )";

}

public String m3()

{

return "Separate.m3()";

}

}

用這樣的類型聲明和類的定義,圖2從概念的觀點描述了Java指令。

Derived2 derived2 = new Derived2();

圖2 :Derived2 對象上的引用

上文中聲明了derived2這個對象,它是Derived2類的。圖2種的最頂層把Derived2引用描述成一個集合的窗口,雖然其下的Derived2對象是可見的。這裏為每個Derived2類型的操作留了一個孔。Derived2對象的每個操作都去映射適當的代碼,按照上麵的代碼所描述的那樣。例如,Derived2對象映射了在Derived中定義的m1()方法。而且還重載了Base類的m1()方法。一個Derived2的引用變量無權訪問Base類中被重載的m1()方法。但這並不意味著不可以用super.m1()的方法調用去使用這個方法。關係到derived2這個引用的變量,這個代碼是不合適的。Derived2的其他的操作映射同樣表明了每種類型操作的代碼執行。

既然你有一個Derived2對象,可以用任何一個Derived2類型的變量去引用它。如圖1所示,Derived, Base和IType都是Derived2的基類。所以,Base類的引用是很有用的。圖3描述了以下語句的概念觀點。

Base base = derived2;

圖3:Base類引用附於Derived2對象之上

雖然Base類的引用不用再訪問m3()和m4(),但是卻不會改變它Derived2對象的任何特征及操作映射。無論是變量derived2還是base,其調用m1()或m2(String)所執行的代碼都是一樣的。

String tmp;

// Derived2 reference (Figure 2)

tmp = derived2.m1(); // tmp is "Derived.m1()"

tmp = derived2.m2( "Hello" ); // tmp is "Derived2.m2( Hello )"

// Base reference (Figure 3)

tmp = base.m1(); // tmp is "Derived.m1()"

tmp = base.m2( "Hello" ); // tmp is "Derived2.m2( Hello )"

兩個引用之所以調用同一個行為,是因為Derived2對象並不知道去調用哪個方法。對象隻知道什麼時候調用,它隨著繼承實現的順序去執行。這樣的順序決定了Derived2對象調用Derived裏的m1()方法,並調用Derived2裏的m2(String)方法。這種結果取決於對象本身的類型,而不是引用的類型。

盡管如此,但不意味著你用derived2和base引用的效果是完全一樣的。如圖3所示,Base的引用隻能看到Base類型擁有的操作。所以,雖然Derived2有對方法m3()和m4()的映射,但是變量base不能訪問這些方法。

String tmp;

// Derived2 reference (Figure 2)

tmp = derived2.m3(); // tmp is "Derived.m3()"

tmp = derived2.m4(); // tmp is "Derived2.m4()"

// Base reference (Figure 3)

tmp = base.m3(); // Compile-time error

tmp = base.m4(); // Compile-time error

運行期的Derived2對象保持了接受m3()和m4()方法的能力。類型的限製使Base的引用不能在編譯期調用這些方法。編譯期的類型檢查像一套鎧甲,保證了運行期對象隻能和正確的操作進行相互作用。換句話說,類型定義了對象間相互作用的邊界。

多態的依附性

類型的一致性是多態的核心。對象上的每一個引用,靜態的類型檢查器都要確認這樣的依附和其對象的層次是一致的。當一個引用成功的依附於另一個不同的對象時,有趣的多態現象就產生了。(嚴格的說,對象類型是指類的定義。)你也可以把幾個不同的引用依附於同一個對象。在開始更有趣的場景前,我們先來看一下下麵的情況為什麼不會產生多態。

多個引用依附於一個對象

圖2和圖3描述的例子是把兩個及兩個以上的引用依附於一個對象。雖然Derived2對象在被依附之後仍保持了變量的類型,但是,圖3中的Base類型的引用依附之後,其功能減少了。結論很明顯:把一個基類的引用依附於派生類的對象之上會減少其能力。

一個開發這怎麼會選擇減少對象能力的方案呢?這種選擇是間接的。假設有一個名為ref的引用依附於一個包含如下方法的類的對象:

public String poly1( Base base )

{

return base.m1();

}

用一個Derived2的參數調用poly(Base)是符合參數類型檢查的:

ref.poly1( derived2 );

方法調用把一個本地Base類型的變量依附在一個引入的對象上。所以,雖然這個方法隻接受Base類型的參數,但Derived2對象仍是允許的。開發這就不必選擇丟失功能的方案。從人眼在通過Derived2對象時所看到的情況,Base類型引用的依附導致了功能的喪失。但從執行的觀點看,每一個傳入poly1(Base)的參數都認為是Base的對象。執行機並不在乎有多個引用指向同一個對象,它隻注重把指向另一個對象的引用傳給方法。這些對象的類型不一致並不是主要問題。執行器隻關心給運行時的對象找到適當的實現。麵向類型的觀點展示了多態的巨大能力。

附於多個對象的引用

讓我們來看一下發生在poly1(Base)中的多態行為。下麵的代碼創建了三個對象,並通過引用傳給poly1(Base):

Derived2 derived2 = new Derived2();

Derived derived = new Derived();