整理 Effective Java 書中 Item 21: Design interfaces for multiple inheritance 心得筆記

主旨

Java 8 引入了 default method,終於讓介面可以新增方法而不會立刻讓現有的實作壞掉。但這個看似萬能的解法,其實潛藏不少風險,特別是對原本沒設計來支援這些方法的舊實作。這篇提醒你:設計介面時最好一開始就想清楚,因為事後「加東西」很可能會讓系統爆炸。

點出問題:default method 的魔法有限

在 Java 8 之前,介面一旦公開出去,就幾乎不能再改。加一個新方法,所有實作都會爆錯。default method 的出現,讓我們「表面上」可以擴充介面,不用動到原本的實作。

但問題來了:

舊有的實作根本不知道這些 default method 存在,也沒準備好要處理它們。

比如說,Collection 介面在 Java 8 加了 removeIf() 方法,它的 default 實作是用 iterator.remove() 去刪除符合條件的元素,程式碼如下:

default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean result = false;
    for (Iterator<E> it = iterator(); it.hasNext(); ) {
        if (filter.test(it.next())) {
            it.remove();
            result = true;
        }
    }
    return result;
}

這段程式對大部分 collection 都 OK,但對某些有「同步處理」需求的實作就出事了。

範例:同步容器的災難

Apache Commons 的 SynchronizedCollection 就是一個例子。它是用一個鎖物件包住內部 collection,每個方法都會先做 synchronized 再委派到內部物件。

問題來了,它沒有 override removeIf(),所以會繼承上面那段預設實作。這代表這個方法裡根本沒做同步處理,直接對內部結構操作。這樣一來,如果兩個 thread 同時存取,會發生什麼事?

  • 最輕微的結果:ConcurrentModificationException
  • 更嚴重的結果:資料錯亂或不明錯誤

劃重點:預設方法不是萬靈丹

  • 加 default method ≠ 安全擴充
  • 舊的實作可能無法正確處理這些新功能
  • 不 override 可能會出 runtime 錯
  • 修改介面就像在手術,動之前要三思

真實世界的例子(Optional)

在 Java 自家實作裡(例如 Collections.synchronizedCollection() 回傳的匿名內部類),他們有特別去 override 新增的 default methods,來加上同步鎖。這是因為 JDK 團隊能控制整體更新節奏。

但對於像 Apache 這樣的第三方函式庫,就沒辦法配合得這麼即時,使用者也常常不自覺踩雷。

小結

default method 給了介面「好像」能加新功能的能力,但這只是糖衣。實際上,它潛藏許多相容性問題。

設計介面的三個提醒:

  1. 介面一釋出,後悔都來不及:所以要先多寫幾個實作、多寫幾個用法測試。
  2. default method 只能加,不能改也不能刪:更不能改方法簽名。
  3. 新增 default method 要特別小心舊有實作的行為與假設

如果你設計了一個介面,請記得:未來的你一定會感謝現在的你有先想過這些問題。