Effective Java Item19:設計可繼承的類別,否則就禁止繼承
整理 Effective Java 書中 Item 19: Design and document for inheritance or else prohibit it 心得筆記
主旨
Java 裡的繼承功能很強,但使用起來也很危險。如果你打算讓別人「繼承你的類別」,你不只是要寫出可以用的 API,還要公開類別的內部行為細節。如果沒做到這一點,繼承後的子類可能會在某次更新中爆掉。
所以這一條的建議是:
如果你沒打算讓別人繼承,就該禁止繼承;
如果你開放繼承,就要設計與文件都做到位。
點出問題:沒設計過的繼承會害死人
先回顧一下上一條 Item 18 的例子:當我們繼承 HashSet 並試圖覆寫 add(),但不知道它的 addAll() 其實會呼叫 add(),導致邏輯錯誤。
這就是所謂的 self-use(自我使用) 問題:一個類別自己內部會呼叫某些可以被子類覆寫的方法,但這些細節如果沒寫清楚,就會讓繼承者出錯,而且他們完全沒辦法預測。
為了解決這個問題,Java 8 引進了 @implSpec 標籤,讓你可以明確記錄「這個方法內部會呼叫哪些可覆寫的方法,以及呼叫順序與邏輯影響」。
範例說明:AbstractCollection 的 remove()
/**
* @implSpec
* 這個方法會透過 iterator() 搭配 iterator.remove() 來移除元素。
* 如果 iterator 不支援 remove 會拋出例外。
*/
public boolean remove(Object o) {
...
}
這樣文件就會清楚告訴你:「如果你改寫了 iterator(),那 remove() 的行為也會跟著變動。」這是讓繼承安全的第一步。
小心設計:你要暴露哪些 protected 成員?
有些時候,為了讓子類有效率地運作,你可能要開放一些 protected 方法或欄位,例如:
// AbstractList 中為了提升 clear() 的效能而開放的鉤子
protected void removeRange(int fromIndex, int toIndex) { ... }
但每開放一個 protected 成員,等於你承諾不會隨便改變它的存在與行為。太多會限制維護,太少會讓繼承者什麼都做不了。怎麼辦?靠測試子類來驗證!
作者建議至少寫三個子類測試這個類別的可擴展性,其中一個還要是「別人寫的」,這樣比較真實。
大忌:建構子裡呼叫可覆寫方法
這是非常常見但危險的錯誤,直接看例子:
public class Super {
public Super() {
overrideMe(); // 呼叫會被覆寫的方法
}
public void overrideMe() { }
}
public class Sub extends Super {
private final Instant instant;
public Sub() {
instant = Instant.now(); // 尚未執行時就會被上面的 overrideMe 呼叫
}
@Override
public void overrideMe() {
System.out.println(instant); // 第一次會印出 null!
}
}
這會讓子類在尚未初始化完成時被強迫執行方法,結果就是空指標、邏輯錯亂、甚至安全漏洞。建構子只能呼叫 private、final 或 static 方法,因為這些不能被子類改寫。
真實世界延伸補充
如果你要讓類別實作
Cloneable或Serializable,更要小心。clone()和readObject()行為就像建構子一樣,不可以在這些方法裡呼叫可被覆寫的方法,否則可能會破壞 clone 的物件或發生讀取失敗。如果你真的要實作
readResolve()或writeReplace()並開放繼承,記得設成protected而不是private,不然子類會被靜悄悄地忽略。
建議策略:不想開放就封起來!
最保險的做法就是:
- 用
final宣告類別,明確禁止被繼承 - 或是把建構子設為
private/package-private,只開放靜態工廠方法(Item 17 有介紹)
若真的要開放繼承,也請:
- 清楚寫下哪些方法會呼叫哪些可覆寫方法
- 小心地設計
protected欄位與鉤子方法 - 寫實際子類測試,確認真能擴充成功
小結
讓一個類別可以被安全地繼承,需要大量設計、測試與文件工作。如果你沒打算處理這些,就應該主動封鎖繼承權限。
這條原則可以簡化成一句話:
❗「沒準備好,就別讓人繼承你的類別!」
別因為方便,讓別人承擔你遺留下來的技術債。