Effective Java 第三版——44. 優先使用標準的函數式接口

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

Effective Java, Third Edition

44. 優先使用標準的函數式接口

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

考慮LinkedHashMap。 能夠經過重寫其受保護的removeEldestEntry方法將此類用做緩存,每次將新的key值加入到map時都會調用該方法。 當此方法返回true時,map將刪除傳遞給該方法的最久條目。 如下代碼重寫容許map增加到一百個條目,而後在每次添加新key值時刪除最老的條目,並保留最近的一百個條目:api

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接口的使用。app

在java.util.Function中有43個接口。不能期望所有記住它們,可是若是記住了六個基本接口,就能夠在須要它們時派生出其他的接口。基本接口操做於對象引用類型。Operator接口表示方法的結果和參數類型相同。Predicate接口表示其方法接受一個參數並返回一個布爾值。Function接口表示方法其參數和返回類型不一樣。Supplier接口表示一個不接受參數和返回值(或「供應」)的方法。最後,Consumer表示該方法接受一個參數而不返回任何東西,本質上就是使用它的參數。六種基本函數式接口概述以下:ide

接口 方法 示例
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中提供的標準接口,但請注意,在相對罕見的狀況下,最好編寫本身的函數式接口。

相關文章
相關標籤/搜索