Thinking in Java(7-3) 深入探討 Java 的協變回傳型別與繼承設計
在 Java 的多型機制中,協變回傳型別和繼承的設計是兩個重要的概念。協變回傳型別允許在覆寫方法時,回傳基礎類別方法回傳型別的子類別。繼承則是物件導向設計的基石,但過度使用繼承可能會導致設計上的複雜性。本篇文章將深入探討這些概念,並透過程式碼範例進行說明。
協變回傳型別(Covariant Return Types)
Java SE5(也就是 Java 5)引入了協變回傳型別的概念,這意味著在衍生類別中覆寫基礎類別的方法時,可以回傳基礎類別回傳型別的子類別。
class Grain {
public String toString() { return "Grain"; }
}
class Wheat extends Grain {
public String toString() { return "Wheat"; }
}
class Mill {
Grain process() { return new Grain(); }
}
class WheatMill extends Mill {
Wheat process() { return new Wheat(); }
}
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
} /* Output:
Grain
Wheat
*/
在上述範例中,Mill
類別的 process()
方法回傳 Grain
物件。在子類別 WheatMill
中,我們覆寫了 process()
方法,並讓其回傳 Wheat
物件(Grain
的子類別)。在 Java SE5 之前,這種做法會導致編譯錯誤,因為覆寫的方法必須回傳與基礎類別相同的型別。
協變回傳型別允許我們在覆寫方法時,回傳更具體的子類別型別,這提高了程式設計的彈性和表達力。這種特性使得覆寫方法更符合直覺,也讓程式碼更具可讀性。
以繼承進行設計
學習了多型之後,可能會覺得所有東西都可以被繼承。然而,在使用現有類別來建立新類別時,如果優先考慮繼承,可能會增加設計的負擔,導致不必要的複雜度。一種更好的方式是優先使用組合(Composition)。
組合不會強迫程式設計進入繼承的層次結構,因此更靈活,也可以動態選擇型別。相反地,繼承在編譯時就需要知道確切的型別。
以下是一個範例:
class Actor {
public void act() {}
}
class HappyActor extends Actor {
public void act() { print("HappyActor"); }
}
class SadActor extends Actor {
public void act() { print("SadActor"); }
}
class Stage {
private Actor actor = new HappyActor();
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
} /* Output:
HappyActor
SadActor
*/
在 Stage
類別中,我們透過組合使用了 Actor
類別的物件。Stage
可以在執行期間隨時更換 Actor
物件,進而改變 performPlay()
方法的行為,達到動態靈活性的效果。
與此相反,我們無法在執行期間改變繼承的類別,因為繼承在編譯時就已經確定。
使用繼承與組合的準則
- 繼承表達行為的差異:透過繼承,我們可以創建不同的子類別,以覆寫方法來表達不同的行為。
- 組合表達狀態的變化:透過組合,我們可以在類別中包含其他物件,並在執行期間改變這些物件,從而改變類別的行為。
在上述範例中,我們透過繼承創建了 HappyActor
和 SadActor
,表達 act()
方法的不同實現。而 Stage
類別透過組合包含了一個 Actor
物件,並能夠在執行期間改變其狀態(actor
),進而改變行為。
純替換與擴充(Substitution vs. Extension)
採取「純粹」的方式建立繼承層次結構,意味著衍生類別只覆寫基礎類別中已經存在的方法,不添加新的方法。
這種方式被稱為純替換,因為衍生類別可以完全替代基礎類別,而在使用時不需要知道子類別的任何額外資訊。

在這種設計中,基礎類別可以接收發送給衍生類別的任何訊息,因為它們具有完全相同的介面。我們只需將衍生類別向上轉型為基礎類別,並透過多型來處理。

然而,許多情況下,「像是某種東西」(is-like-a)的關係更適合解決特定問題。也就是說,子類別除了具有基礎類別的介面外,還擁有額外的方法和特性。

但需要注意的是,當我們向上轉型為基礎類別時,子類別中擴充的部分將無法被訪問。如果我們需要使用子類別中特有的方法,必須進行向下轉型。

向下轉型與執行時型別識別(RTTI)
向上轉型會遺失物件的具體類型資訊,而向下轉型可以重新獲取這些資訊。然而,向下轉型並非總是安全的,因為我們無法保證一個基礎類別的引用實際上是一個特定的子類別。
在某些程式語言中,需要透過特定的機制來確保向下轉型的正確性。在 Java 中,所有的轉型都會在執行時進行檢查,以確保物件的型別是否符合轉型的要求。如果不符合,將拋出 ClassCastException
。
這種在執行期間對型別進行檢查的機制稱為 執行時型別識別(Runtime Type Identification,RTTI)。
以下是一個範例:
class Useful {
public void f() {}
public void g() {}
}
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile time: method not found in Useful:
//! x[1].u();
((MoreUseful)x[1]).u(); // Downcast/RTTI
((MoreUseful)x[0]).u(); // Exception thrown
}
}
在這個例子中,我們嘗試對 x[1]
進行向下轉型,因為我們知道它實際上是 MoreUseful
的實例。然而,對 x[0]
進行同樣的轉型將導致 ClassCastException
,因為 x[0]
是 Useful
的實例,並不是 MoreUseful
。
因此,RTTI 不僅包括轉型處理,還提供了一種方法,讓我們在試圖向下轉型之前,檢查所處理物件的實際類型。
總結
協變回傳型別讓我們在覆寫方法時,可以回傳更具體的子類別型別,增加了程式設計的靈活性。另一方面,繼承和組合是物件導向設計中兩種基本的復用方式。過度使用繼承可能導致複雜的繼承層次結構,建議在設計時優先考慮組合。向下轉型和執行時型別識別(RTTI)允許我們在需要時重新獲取物件的具體類型資訊,但必須謹慎使用,避免轉型錯誤。
Comments ()