James Gosling(Java 之父)曾公開表示:若能重來,他會捨棄 class 繼承關係。優秀的設計者普遍避免使用 extends(實作繼承),轉而採用 implements(介面繼承)。本文深入剖析繼承帶來的三大核心問題:失去彈性、緊耦合、脆弱基底類別,並展示介面導向設計如何解決這些痛點。
// 緊耦合版本:到處傳遞 LinkedList
void f() {
LinkedList list = new LinkedList();
g(list);
}
void g(LinkedList list) {
list.add(...);
g2(list);
}
若未來需求變更,需改用 HashSet 提升查詢效能,所有參數為 LinkedList 的方法簽名都必須修改,連鎖影響極大。
// 鬆耦合版本:參數型別為 Collection 介面
void f() {
Collection list = new LinkedList(); // 只在建立處指定具體類別
g(list);
}
void g(Collection list) {
list.add(...);
g2(list);
}
現在替換為 new HashSet() 僅需修改建立物件的那一行,其餘代碼完全不動。這就是「針對介面程式設計」帶來的彈性。
// 傳遞 Iterator 介面,支援任何可疊代資料來源
void g2(Iterator i) {
while (i.hasNext()) doSomething(i.next());
}
// 呼叫端
g2(c.iterator()); // Collection
g2(map.keySet().iterator()); // Map 鍵集
g2(customIterator()); // 自訂資料來源(檔案、測試資料、網路串流)
此設計達到極致彈性:方法完全不在乎資料來源,只關心「能疊代」。
繼承建立了極強的靜態耦合:子類別依賴父類別的內部實作細節。類似全域變數的副作用,父類別任何修改都可能波及所有子類別及使用它們的代碼。
物件導向核心原則:封裝實作細節。所有實例變數應為 private,避免 getter/setter 洩露內部結構。繼承直接違反此原則,將父類別受保護/公開成員暴露給子類別,形成「脆弱的基底類別」問題。
class Stack extends ArrayList {
private int top = 0;
public void push(Object o) { add(top++, o); }
public Object pop() { return remove(--top); }
public void pushMany(Object[] arr) {
for (Object o : arr) push(o);
}
}
災難場景:使用者呼叫繼承來的 clear() 方法:
Stack s = new Stack();
s.push("1"); s.push("2");
s.clear(); // ArrayList 清空內部陣列,但 top 指標仍為 2!
s.push("3"); // 實際寫入 index 2,前兩個位置成垃圾資料
父類別 ArrayList 完全不知情 top 狀態,導致子類別不變量被破壞。
top。缺點:若覆寫全部,父類別應為介面;且無法合理實作 removeRange() 等不適用方法(只能拋例外,將編譯錯誤延遲至執行期)。class Stack {
private final List data = new ArrayList(); // 組合
private int top = 0;
public void push(Object o) { data.add(top++, o); }
public Object pop() { return data.remove(--top); }
// 不暴露 List 任何方法,完全掌控行為
}
即使組合版 Stack 設計良好,若再衍生 MonitorableStack extends Stack 並覆寫 push/pop 監控高水位,會發現 pushMany() 內部呼叫 push(),導致計數重複遞增。這證明:繼承破壞封裝,子類別依賴父類別內部實作細節(如方法呼叫鏈)。
| 原則 | 說明 |
|---|---|
| 介面定義契約 | 使用 interface 定義「能做什麼」,而非「怎麼做」 |
| 組合重用程式碼 | 以 has-a 關係持有介面實作,委派具體行為 |
| 依賴注入 | 建構子/設值器注入介面實作,而非硬編碼具體類別 |
| 僅在「是一種」關係極穩定時用繼承 | 如框架內部模板方法模式,且文件明確允許擴充 |
Java 設計模式核心精神:介面繼承優於實作繼承。採用介面導向設計 + 組合重用,能構建低耦合、高彈性、易測試、易維護的系統。下次想寫 class A extends B 時,請先問自己:「這真的是 is-a 關係嗎?能否改用介面 + 組合?」