整理 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 方法,因為這些不能被子類改寫。

真實世界延伸補充

  • 如果你要讓類別實作 CloneableSerializable,更要小心。clone()readObject() 行為就像建構子一樣,不可以在這些方法裡呼叫可被覆寫的方法,否則可能會破壞 clone 的物件或發生讀取失敗。

  • 如果你真的要實作 readResolve()writeReplace() 並開放繼承,記得設成 protected 而不是 private,不然子類會被靜悄悄地忽略。

建議策略:不想開放就封起來!

最保險的做法就是:

  • final 宣告類別,明確禁止被繼承
  • 或是把建構子設為 private / package-private,只開放靜態工廠方法(Item 17 有介紹)

若真的要開放繼承,也請:

  • 清楚寫下哪些方法會呼叫哪些可覆寫方法
  • 小心地設計 protected 欄位與鉤子方法
  • 寫實際子類測試,確認真能擴充成功

小結

讓一個類別可以被安全地繼承,需要大量設計、測試與文件工作。如果你沒打算處理這些,就應該主動封鎖繼承權限。

這條原則可以簡化成一句話:

❗「沒準備好,就別讓人繼承你的類別!」

別因為方便,讓別人承擔你遺留下來的技術債。