Effective Java Item21:設計介面要有前瞻性
整理 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()
去刪除符合條件的元素,程式碼如下:
這段程式對大部分 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 給了介面「好像」能加新功能的能力,但這只是糖衣。實際上,它潛藏許多相容性問題。
設計介面的三個提醒:
- 介面一釋出,後悔都來不及:所以要先多寫幾個實作、多寫幾個用法測試。
- default method 只能加,不能改也不能刪:更不能改方法簽名。
- 新增 default method 要特別小心舊有實作的行為與假設。
如果你設計了一個介面,請記得:未來的你一定會感謝現在的你有先想過這些問題。