1. 程式人生 > >Effective Java 第三版——44. 優先使用標準的函式式介面

Effective Java 第三版——44. 優先使用標準的函式式介面

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨著Java 6,7,8,甚至9的釋出,Java語言發生了深刻的變化。
在這裡第一時間翻譯成中文版。供大家學習分享之用。

Effective Java, Third Edition

44. 優先使用標準的函式式介面

現在Java已經有lambda表示式,編寫API的最佳實踐已經發生了很大的變化。 例如,模板方法模式[Gamma95],其中一個子類重寫原始方法以專門化其父類的行為,變得沒有那麼吸引人。 現代替代的選擇是提供一個靜態工廠或構造方法來接受函式物件以達到相同的效果。 通常地說,可以編寫更多以函式物件為引數的構造方法和方法。 選擇正確的函式式引數型別需要注意。

考慮LinkedHashMap。 可以通過重寫其受保護的removeEldestEntry方法將此類用作快取,每次將新的key值加入到map時都會呼叫該方法。 當此方法返回true時,map將刪除傳遞給該方法的最久條目。 以下程式碼重寫允許map增長到一百個條目,然後在每次新增新key值時刪除最老的條目,並保留最近的一百個條目:

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

這種技術很有效,但是你可以用lambdas做得更好。如果LinkedHashMap

是現在編寫的,那麼它將有一個靜態的工廠或構造方法來獲取函式物件。檢視removeEldestEntry方法的宣告,你可能會認為函式物件應該接受一個Map.Entry <K,V>並返回一個布林值,但是這並不完全是這樣:removeEldestEntry方法呼叫size()方法來獲取條目的數量,因為removeEldestEntry是map上的一個例項方法。傳遞給構造方法的函式物件不是map上的例項方法,無法捕獲,因為在呼叫其工廠或構造方法時map還不存在。因此,map必須將自己傳遞給函式物件,函式物件把map以及最就的條目作為輸入引數。如果要宣告這樣一個功能介面,應該是這樣的:

// Unnecessary functional interface; use a standard one instead.
@FunctionalInterface interface EldestEntryRemovalFunction<K,V>{
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

這個介面可以正常工作,但是你不應該使用它,因為你不需要為此目的宣告一個新的介面。 java.util.function包提供了大量標準函式式介面供你使用。 如果其中一個標準函式式介面完成這項工作,則通常應該優先使用它,而不是專門構建的函式式介面。 這將使你的API更容易學習,通過減少其不必要概念,並將提供重要的互操作性好處,因為許多標準函式式介面提供了有用的預設方法。 例如,Predicate介面提供了組合判斷的方法。 在我們的LinkedHashMap示例中,標準的BiPredicate<Map<K,V>, Map.Entry<K,V>>介面應優先於自定義的EldestEntryRemovalFunction介面的使用。

在java.util.Function中有43個介面。不能指望全部記住它們,但是如果記住了六個基本介面,就可以在需要它們時派生出其餘的介面。基本介面操作於物件引用型別。Operator介面表示方法的結果和引數型別相同。Predicate介面表示其方法接受一個引數並返回一個布林值。Function介面表示方法其引數和返回型別不同。Supplier介面表示一個不接受引數和返回值(或“供應”)的方法。最後,Consumer表示該方法接受一個引數而不返回任何東西,本質上就是使用它的引數。六種基本函式式介面概述如下:

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

在處理基本型別int,long和double的操作上,六個基本介面中還有三個變體。 它們的名字是通過在基本介面前加一個基本型別而得到的。 因此,例如,一個接受int的Predicate是一個IntPredicate,而一個接受兩個long值並返回一個long的二元運算子是一個LongBinaryOperator。 除Function介面變體通過返回型別進行了引數化,其他變體型別都沒有引數化。 例如,LongFunction<int[]>使用long型別作為引數並返回了int []型別。

Function介面還有九個額外的變體,當結果型別為基本型別時使用。 源和結果型別總是不同,因為從型別到它自身的函式是UnaryOperator。 如果源型別和結果型別都是基本型別,則使用帶有SrcToResult的字首Function,例如LongToIntFunction(六個變體)。如果源是一個基本型別,返回結果是一個物件引用,那麼帶有<Src>ToObj的字首Function,例如DoubleToObjFunction (三種變體)。

有三個包含兩個引數版本的基本功能介面,使它們有意義:BiPredicate <T,U>BiFunction <T,U,R>BiConsumer <T,U>。 也有返回三種相關基本型別的BiFunction變體:ToIntBiFunction <T,U>ToLongBiFunction <T,U>ToDoubleBiFunction <T,U>Consumer有兩個變數,它們帶有一個物件引用和一個基本型別:ObjDoubleConsumer <T>ObjIntConsumer <T>ObjLongConsumer <T>。 總共有九個兩個引數版本的基本介面。

最後,還有一個BooleanSupplier介面,它是Supplier的一個變體,它返回布林值。 這是任何標準函式式介面名稱中唯一明確提及的布林型別,但布林返回值通過Predicate及其四種變體形式支援。 前面段落中介紹的BooleanSupplier介面和42個接口占所有四十三個標準功能介面。 無可否認,這是非常難以接受的,並且不是非常正交的。 另一方面,你所需要的大部分功能介面都是為你寫的,而且它們的名字是經常性的,所以在你需要的時候不應該有太多的麻煩。

大多數標準函式式介面僅用於提供對基本型別的支援。 不要試圖使用基本的函式式介面來裝箱基本型別的包裝類而不是基本型別的函式式介面。 雖然它起作用,但它違反了第61條中的建議:“優先使用基本型別而不是基本型別的包裝類”。使用裝箱基本型別的包裝類進行批量操作的效能後果可能是致命的。

現在你知道你應該通常使用標準的函式式介面來優先編寫自己的介面。 但是,你應該什麼時候寫自己的介面? 當然,如果沒有一個標準模組能夠滿足您的需求,例如,如果需要一個帶有三個引數的Predicate,或者一個丟擲檢查異常的Predicate,那麼需要編寫自己的程式碼。 但有時候你應該編寫自己的函式式介面,即使與其中一個標準的函式式介面的結構相同。

考慮我們的老朋友Comparator <T>,它的結構與ToIntBiFunction <T, T>介面相同。 即使將前者新增到類庫時後者的介面已經存在,使用它也是錯誤的。 Comparator值得擁有自己的介面有以下幾個原因。 首先,它的名稱每次在API中使用時都會提供優秀的文件,並且使用了很多。 其次,Comparator介面對構成有效例項的構成有強大的要求,這些要求構成了它的普遍契約。 通過實現介面,就要承諾遵守契約。 第三,介面配備很多了有用的預設方法來轉換和組合多個比較器。

如果需要一個函式式介面與Comparator共享以下一個或多個特性,應該認真考慮編寫一個專用函式式介面,而不是使用標準函式式介面:

  • 它將被廣泛使用,並且可以從描述性名稱中受益。
  • 它擁有強大的契約。
  • 它會受益於自定義的預設方法。

如果選擇編寫你自己的函式式介面,請記住它是一個介面,因此應非常小心地設計(條目 21)。

請注意,EldestEntryRemovalFunction介面(第199頁)標有@FunctionalInterface註解。 這種註解在型別類似於@Override。 這是一個程式設計師意圖的陳述,它有三個目的:它告訴讀者該類和它的文件,該介面是為了實現lambda表示式而設計的;它使你保持可靠,因為除非只有一個抽象方法,否則介面不會編譯; 它可以防止維護人員在介面發生變化時不小心地將抽象方法新增到介面中。 始終使用@FunctionalInterface註解標註你的函式式介面

最後一點應該是關於在api中使用函式介面的問題。不要提供具有多個過載的方法,這些過載在相同的引數位置上使用不同的函式式介面,如果這樣做可能會在客戶端中產生歧義。這不僅僅是一個理論問題。ExecutorServicesubmit方法可以採用Callable<T>Runnable介面,並且可以編寫需要強制型別轉換以指示正確的過載的客戶端程式(條目 52)。避免此問題的最簡單方法是不要編寫在相同的引數位置中使用不同函式式介面的過載。這是條目52中建議的一個特例,“明智地使用過載”。

總之,現在Java已經有了lambda表示式,因此必須考慮lambda表示式來設計你的API。 在輸入上接受函式式介面型別並在輸出中返回它們。 一般來說,最好使用java.util.function.Function中提供的標準介面,但請注意,在相對罕見的情況下,最好編寫自己的函式式介面。