整理 Effective Java 書中 Item 44: Use standard functional interfaces 心得筆記

主旨:學會選擇正確的標準函式介面

從 Java 8 開始,因為 lambda 的加入,我們寫 API 的思維也隨之改變。如果你需要傳入一段行為(function object),不再需要額外建立匿名類別,而是可以用 lambda 寫得更簡潔。但要寫出真正好用的 API,選對函式介面更是關鍵——盡可能用 Java 內建的標準函式介面,能讓程式碼更直觀、可讀性更高,也方便 IDE 自動補全與檢查。

點出問題:不該每次都自創介面

假設你想要建立一個可限制最大筆數的快取,會用 LinkedHashMap 來實作,並覆寫 removeEldestEntry()

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > 100;
}

但如果要將這段行為抽成參數傳入 constructor,應該怎麼設計呢?

直覺會想自訂一個像這樣的介面:

@FunctionalInterface
interface EldestEntryRemovalFunction<K, V> {
    boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}

這樣雖然可行,但會造成 API 多了不必要的學習成本,而且失去了與現成工具(如 Streams、Collections)整合的優勢。

範例:善用 BiPredicate 就夠了

事實上,上面這個介面可以直接用 Java 內建的 BiPredicate<Map<K,V>, Map.Entry<K,V>> 取代:

BiPredicate<Map<K, V>, Map.Entry<K, V>> removalPolicy =
    (map, eldest) -> map.size() > 100;

你根本不需要額外創建 EldestEntryRemovalFunction 這種客製化介面,只要用標準的 BiPredicate,程式碼清楚簡潔又具可讀性。

劃重點:掌握六大核心標準函式介面

其實你只要熟悉下面這六種函式介面,就能應付大部分情境:

介面函數原型範例
Predicate<T>boolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
Supplier<T>T get()Instant::now
Consumer<T>void accept(T t)System.out::println
UnaryOperator<T>T apply(T t)String::toLowerCase
BinaryOperator<T>T apply(T t1, T t2)BigInteger::add

此外,還有針對 int, long, double 的變型,如 IntPredicate, LongFunction<R>,總共 43 種。名稱都很規則,只要記住邏輯,要查詢並不難。

真實世界範例:Comparator 就是自訂介面的好例子

雖然我們鼓勵使用標準介面,但也不是「一刀切」。像是 Comparator<T> 就是自訂介面的好例子,雖然它與 ToIntBiFunction<T, T> 結構相同,但:

  • 名稱具說明性
  • 有清楚的契約(例如:要實作 compare,需要滿足自反性、對稱性)
  • 有許多實用的 default methods(例如:thenComparing()

只要滿足這三個條件之一,就值得自訂介面來提高 API 可用性與可讀性。

小結:能用標準的就不要自創

若要讓 API 更現代、清晰、好維護,選用標準函式介面是很關鍵的一步:

  • ✅ 優先使用 java.util.function 的標準介面
  • 🔍 自訂介面需具備:好名字、有契約、需要擴充 method
  • 📌 使用 @FunctionalInterface 註解以防後續誤用
  • 🧼 避免在同一參數位置 overload 多種不同函式介面,會造成使用者混淆