effective java 3 - 第7章 lambda和stream[44] 堅持使用標準的函數(shù)接口
????在Java具有Lambda 表達式之后,編寫API 的最佳實踐也做了相應(yīng)的改變。例如在模板方法(Tmplate Method)模式[Gamma95]中,用一個子類覆蓋基本類型方法(primitive method),來限定其超類的行為,這是最不討人喜歡的?,F(xiàn)在的替代方法是提供一個接受函數(shù)對象的靜態(tài)工廠或者構(gòu)造器,便可達到同樣的效果。在大多數(shù)情況下,需要編寫更多的構(gòu)造器和方法,以函數(shù)對象作為參數(shù)。需要非常謹慎地選擇正確的函數(shù)參數(shù)類型。
????以LinkedHashMap為例。每當有新的鍵添加到映射中時,put 就會調(diào)用其受保護的 removeEldestEntry方法。如果覆蓋該方法,便可以用這個類作為緩存。當該方法返回true,映射就會刪除最早傳入方法的條目。下列覆蓋代碼允許映射增長到100個條目,然后每添加一個新的鍵,就會刪除最早的那個條目,始終保持最新的100個條目:
? ? ?這個方法很好用,但是用Lambda 可以完成得更漂亮。假如現(xiàn)在編寫LinkedHashMap,它會有一個帶函數(shù)對象的靜態(tài)工廠或者構(gòu)造器。看一下 removeEldestEntry 的聲明,你可能會以為該函數(shù)對象應(yīng)該帶一個Map.Entry<K,V> ,并且返回一個boolean,但實際并非如此:removeEldestEntry 會調(diào)用size() ,獲取映射中的條目數(shù)量,這是因為?removeEldestEntry 是映射中的一個實例方法。傳到構(gòu)造器中的函數(shù)對象則不是映射中的實例方法,無法捕捉到,因為調(diào)用其工廠或者構(gòu)造器時,這個映射還不存在。所以,映射必須將它自身傳給函數(shù)對象,因此必須傳入映射及其最早的條目作為 remove方法的參數(shù)。聲明一個這樣的函數(shù)接口的代碼如下:
????這個接口可以正常工作,但是不應(yīng)該使用,因為沒必要為此聲明。java.util.function 包已經(jīng)為此提供了大量標準的函數(shù)接口。 只要標準的函數(shù)接口能夠滿足需求,通常應(yīng)該優(yōu)先考慮,而不是專門再構(gòu)建一個新的函數(shù)接口。這樣會使API 更加容易學(xué)習,通常減少它的概念內(nèi)容,顯著提升互操作性優(yōu)勢,因為許多標準的函數(shù)接口都提供了有用的默認方法。如 Predicate 接口提供了合并斷言的方法。對于上述 LinkedHashMap范例,應(yīng)該優(yōu)先使用標準的 BiPredicate<Map<K,V> ,Map.Entry<K,V>> 接口,而不是定制 EldestEntryRemovalFunction 接口。
????java.util.function 中共有43個接口。別指望能夠全部記住它們,但是如果能記住其中6個基礎(chǔ)接口,必要時就可以推斷出其余接口了?;A(chǔ)接口作用域?qū)ο笠妙愋?。Operator 接口代表其結(jié)果與參數(shù)類型一致的函數(shù)。Predicate 接口代表帶有一個參數(shù)的并返回一個boolean? 的函數(shù)。Function 接口 代表其參數(shù)與返回的類型不一致的函數(shù)。 Supplier 接口代表沒有參數(shù)并且返回(或“提供”)一個值的函數(shù)。最后,Consumer 代表的是帶有一個函數(shù)但不返回任何值的函數(shù),相當于消費掉了其參數(shù)。這6個基礎(chǔ)函數(shù)表述如下

????這6個基礎(chǔ)接口各自還有3種變體,分別可以作用于基本類型 int、long、double。它們的命名方式是在其基礎(chǔ)接口前面加上基本類型。因此,以帶有 int 的predicate接口為例,其變體名稱應(yīng)該是 IntPredicate。 這些變體接口的類型都不是參數(shù)化的,除了Function 變體外,后者是以返回類型為參數(shù)。例如,LongFunction<int[]> 表示帶有一個long參數(shù),并返回一個int[] 數(shù)組。
????Function接口還有9種變體,用于結(jié)果類型為基本類型的情況。源類型和結(jié)果類型始終不一樣,因為從類型到自身的函數(shù)就是UnaryOperator。如果源類型和結(jié)果類型均為基本類型,就是在Function前面添加格式如 SrcToResult 如 LongToIntFunction(有6種變體)。如果源類型為基本類型,結(jié)果類型是一個對象參數(shù),則要在Function前面添加 <Src>ToObj,如 DoubleToObjFunction(3種變體)。
????這3種基礎(chǔ)函數(shù)接口還有帶兩個參數(shù)的版本,如BiPredicate<T,U> 、BiFunction<T,U,R> 和BiConsumer<T,U>。還有BiFunction變體用于返回三個相關(guān)的基本類型:ToIntBiFunction<T,U> ,ToLongBiFunction<T,U>和 ToDoubleBiFunction<T,U> 。Consumer接口也有帶兩個參數(shù)的變體版本,他們帶一個對象引用和一個基本類型 ObjDoubleConsumer<T>? ,ObjIntConsumer<T> , ObjLongConsumer<T> ??傊?,這些基礎(chǔ)接口有9個帶兩個參數(shù)的版本。
????最后,還有BooleanSupplier接口,它是Supplier 接口的一種變體,返回boolean值。這是在所有的標準函數(shù)接口名稱中唯一顯式提到boolean 類型的,但boolean返回值是通過Predicate 及其4種變體來支持的。BooleanSupplier接口和上述提到的42個接口,總計43個標準函數(shù)接口。顯然,這是個大數(shù)字,但是他們之間并非縱橫交錯。另一方面,你需要的接口函數(shù)都替你寫好了,它們的名稱都是循規(guī)蹈矩的,需要的時候并不難找到。
????現(xiàn)有的大多數(shù)標準函數(shù)接口都只支持基本類型。千萬不要用帶包裝類型的基礎(chǔ)函數(shù)接口來代替函數(shù)接口。雖然可行,但它破壞了第61條的規(guī)則“基本類型優(yōu)于裝箱基本類型”。使用裝箱基本類型進行批量操作處理,最終會導(dǎo)致致命的性能問題。
????現(xiàn)在知道了,通常應(yīng)該優(yōu)先使用標準的函數(shù)接口,而不是用自己編寫的接口。但什么時候一聽該自己編寫接口呢?當然是在如果沒有任何標準的函數(shù)接口能滿足你的需求之時,如果需要一個帶有3個參數(shù)的Predicate接口,或者需要一個拋出受檢異常的接口時,當然就需要自己編寫啦。但是也有這樣的情況:有結(jié)構(gòu)相同的標準函數(shù)接口可用,卻還是應(yīng)該自己編寫函數(shù)接口。
????還是以咱們的老朋友 Comparator<T>為例。它與ToIntBiFunction<T,T>接口在結(jié)構(gòu)上一致,雖然前者被添加到類庫中時,后一個接口已經(jīng)存在,但如果用后者就錯了。COmparator之所以需要自己的接口,有3個原因。首先,每當在API中使用時,其名稱提供了良好的文檔信息,并且被大量使用。其次,Comparator接口對于如何構(gòu)成一個有效的實例,有著嚴格的條件限制,這構(gòu)成了它的總則(genneral contracat)。實現(xiàn)該接口相當于承諾遵守其契約。第三,這個接口配置了大量很好用的缺省方法,可以對比較器進行轉(zhuǎn)換和合并。
????如果你所需要的函數(shù)接口與Comparator一樣具有一項或者多項以下特征,則必須認真考慮自己編寫專用的函數(shù)接口,而不是使用標準的函數(shù)接口:
通用,并且將受益于描述性的名稱。
具有與其關(guān)聯(lián)的嚴格的契約
將受益于定制的缺省方法
????如果決定自己編寫函數(shù)接口,一定要記住,它是一個接口,因而設(shè)計時應(yīng)當萬分謹慎(詳見第21條)。
????注意,EldestEntryRemovalFunction接口是用 @FunctionalInterface 注解進行標注的。這個注解類型本質(zhì)上與@Override類似。這是一個標注了程序員設(shè)計意圖的語句,它有3個目的:告訴這個類及其文檔的讀者,這個接口是針對Lambda設(shè)計的;這個接口不會進行編譯,除非它只有一個抽象方法;避免后續(xù)維護人員不小心給該接口添加抽象方法。必須始終用 @FunctionalInterface 注解對自己編寫的函數(shù)接口進行標注。
????最后一點是關(guān)于函數(shù)接口在API中的使用。不要在相同的參數(shù)位置,提供不同的函數(shù)接口來進行多次重載的方法,否則可能在客戶端導(dǎo)致歧義。這不僅僅是理論上的問題。比如ExecutorService的submit方法就可能帶有Callable<T>或者Runnable,并且還可以編寫一個客戶端程序,要求進行一次轉(zhuǎn)換,以顯示正確的重載(詳見第52條)。避免這個問題的最簡單方式是,不要編寫在同一個參數(shù)位置使用不同函數(shù)接口的重載。這是該建議的一個特例,詳情見52條。
????總而言之,既然Java有了Lambda,就必須時刻謹記用Lambda來設(shè)計API。輸入時接受函數(shù)接口類型,并在輸出時返回之。一般來說,最好使用java.util.function.Function中提供的標準接口,但是必須警惕在相對罕見的幾種情況下,最好還是自己編寫專用的函數(shù)接口。