Item 44: Favor the use of standard functional interfaces(優先使用標準函數式接口)

Now that Java has lambdas, best practices for writing APIs have changed considerably. For example, the Template Method pattern [Gamma95], wherein a subclass overrides a primitive method to specialize the behavior of its superclass, is far less attractive. The modern alternative is to provide a static factory or constructor that accepts a function object to achieve the same effect. More generally, you’ll be writing more constructors and methods that take function objects as parameters. Choosing the right functional parameter type demands care.

現在 Java 已經有了 lambda 表達式,編寫 API 的最佳實踐已經發生了很大的變化。例如,模板方法模式 [Gamma95],其中子類覆蓋基類方法以專門化其超類的行爲,就沒有那麼有吸引力了。現代的替代方法是提供一個靜態工廠或構造函數,它接受一個函數對象來實現相同的效果。更一般地,你將編寫更多以函數對象爲參數的構造函數和方法。選擇正確的函數參數類型需要謹慎。

Consider LinkedHashMap. You can use this class as a cache by overriding its protected removeEldestEntry method, which is invoked by put each time a new key is added to the map. When this method returns true, the map removes its eldest entry, which is passed to the method. The following override allows the map to grow to one hundred entries and then deletes the eldest entry each time a new key is added, maintaining the hundred most recent entries:

考慮 LinkedHashMap。你可以通過覆蓋受保護的 removeEldestEntry 方法將該類用作緩存,每當向映射添加新鍵時,put 都會調用該方法。當該方法返回 true 時,映射將刪除傳遞給該方法的最老條目。下面的覆蓋允許映射增長到 100 個條目,然後在每次添加新鍵時刪除最老的條目,維護 100 個最近的條目:

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

This technique works fine, but you can do much better with lambdas. If LinkedHashMap were written today, it would have a static factory or constructor that took a function object. Looking at the declaration for removeEldestEntry, you might think that the function object should take a Map.Entry<K,V> and return a boolean, but that wouldn’t quite do it: The removeEldestEntry method calls size() to get the number of entries in the map, which works because removeEldestEntry is an instance method on the map. The function object that you pass to the constructor is not an instance method on the map and can’t capture it because the map doesn’t exist yet when its factory or constructor is invoked. Thus, the map must pass itself to the function object, which must therefore take the map on input as well as its eldest entry. If you were to declare such a functional interface, it would look something like this:

這種技術工作得很好,但是使用 lambda 表達式可以做得更好。如果 LinkedHashMap 是現在編寫的,它將有一個靜態工廠或構造函數,它接受一個函數對象。看着 removeEldestEntry 的定義,你可能會認爲這個函數對象應該 Map.Entry<K,V> 和返回一個布爾值,但不會完全做到:removeEldestEntry 方法調用 size() 地圖中的條目的數量,這工作,因爲 removeEldestEntry 在 Map 上是一個實例方法。傳遞給構造函數的函數對象不是 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);
}

This interface would work fine, but you shouldn’t use it, because you don’t need to declare a new interface for this purpose. The java.util.function package provides a large collection of standard functional interfaces for your use. If one of the standard functional interfaces does the job, you should generally use it in preference to a purpose-built functional interface. This will make your API easier to learn, by reducing its conceptual surface area, and will provide significant interoperability benefits, as many of the standard functional interfaces provide useful default methods. The Predicate interface, for instance, provides methods to combine predicates. In the case of our LinkedHashMap example, the standard BiPredicate<Map<K,V>, Map.Entry<K,V>> interface should be used in preference to a custom EldestEntryRemovalFunction interface.

這個接口可以很好地工作,但是你不應該使用它,因爲你不需要爲此聲明一個新接口。java.util.function 包提供了大量的標準函數接口供你使用。如果一個標準的函數式接口可以完成這項工作,那麼你通常應該優先使用它,而不是使用專門構建的函數式接口。 通過減少 API 的概念表面積,這將使你的 API 更容易學習,並將提供顯著的互操作性優勢,因爲許多標準函數式接口提供了有用的默認方法。例如,Predicate 接口提供了組合謂詞的方法。在我們的 LinkedHashMap 示例中,應該優先使用標準的 BiPredicate<Map<K,V>Map.Entry<K,V>> 接口,而不是定製的 EldestEntryRemovalFunction 接口。

There are forty-three interfaces in java.util.Function. You can’t be expected to remember them all, but if you remember six basic interfaces, you can derive the rest when you need them. The basic interfaces operate on object reference types. The Operator interfaces represent functions whose result and argument types are the same. The Predicate interface represents a function that takes an argument and returns a boolean. The Function interface represents a function whose argument and return types differ. The Supplier interface represents a function that takes no arguments and returns (or “supplies”) a value. Finally, Consumer represents a function that takes an argument and returns nothing, essentially consuming its argument. The six basic functional interfaces are summarized below:

譯註:原文筆誤,應爲 java.util.function

java.util.function 中有 43 個接口。不能期望你記住所有的接口,但是如果你記住了 6 個基本接口,那麼你可以在需要時派生出其餘的接口。基本接口操作對象引用類型。Operator 接口表示結果和參數類型相同的函數。Predicate 接口表示接受參數並返回布爾值的函數。Function 接口表示參數和返回類型不同的函數。Supplier 接口表示一個不接受參數並返回(或「供應」)值的函數。最後,Consumer 表示一個函數,該函數接受一個參數,但不返回任何內容,本質上是使用它的參數。六個基本的函數式接口總結如下:

Interface Function Signature Example
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
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

There are also three variants of each of the six basic interfaces to operate on the primitive types int, long, and double. Their names are derived from the basic interfaces by prefixing them with a primitive type. So, for example, a predicate that takes an int is an IntPredicate, and a binary operator that takes two long values and returns a long is a LongBinaryOperator. None of these variant types is parameterized except for the Function variants, which are parameterized by return type. For example, LongFunction<int[]> takes a long and returns an int[].

還有 6 個基本接口的 3 個變體,用於操作基本類型 int、long 和 double。它們的名稱是通過在基本接口前面加上基本類型前綴而派生出來的。例如,一個接受 int 的 Predicate 就是一個 IntPredicate,一個接受兩個 long 值並返回一個 long 的二元操作符就是一個 LongBinaryOperator。除了由返回類型參數化的函數變量外,這些變量類型都不是參數化的。例如,LongFunction<int[]> 使用 long 並返回一個 int[]。

There are nine additional variants of the Function interface, for use when the result type is primitive. The source and result types always differ, because a function from a type to itself is a UnaryOperator. If both the source and result types are primitive, prefix Function with SrcToResult, for example LongToIntFunction (six variants). If the source is a primitive and the result is an object reference, prefix Function with <Src>ToObj, for example DoubleToObjFunction (three variants).

Function 接口還有 9 個額外的變體,在結果類型爲基本數據類型時使用。源類型和結果類型總是不同的,因爲不同類型的函數本身都是 UnaryOperator。如果源類型和結果類型都是基本數據類型,則使用帶有 SrcToResult 的前綴函數,例如 LongToIntFunction(六個變體)。如果源是一個基本數據類型,而結果是一個對象引用,則使用帶前綴 <Src>ToObj 的 Function 接口,例如 DoubleToObjFunction(三個變體)。

There are two-argument versions of the three basic functional interfaces for which it makes sense to have them: BiPredicate<T,U>, BiFunction<T,U,R>, and BiConsumer<T,U>. There are also BiFunction variants returning the three relevant primitive types: ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, and ToDoubleBiFunction<T,U>. There are two-argument variants of Consumer that take one object reference and one primitive type: ObjDoubleConsumer<T>, ObjIntConsumer<T>, and ObjLongConsumer<T>. In total, there are nine two-argument versions of the basic interfaces.

三個基本函數式接口有兩個參數版本,使用它們是有意義的: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>。總共有9個基本接口的雙參數版本。

Finally, there is the BooleanSupplier interface, a variant of Supplier that returns boolean values. This is the only explicit mention of the boolean type in any of the standard functional interface names, but boolean return values are supported via Predicate and its four variant forms. The BooleanSupplier interface and the forty-two interfaces described in the previous paragraphs account for all forty-three standard functional interfaces. Admittedly, this is a lot to swallow, and not terribly orthogonal. On the other hand, the bulk of the functional interfaces that you’ll need have been written for you and their names are regular enough that you shouldn’t have too much trouble coming up with one when you need it.

最後是 BooleanSupplier 接口,它是 Supplier 的一個變體,返回布爾值。這是在任何標準函數接口名稱中唯一顯式提到布爾類型的地方,但是通過 Predicate 及其四種變體形式支持布爾返回值。前面描述的 BooleanSupplier 接口和 42 個接口占了全部 43 個標準函數式接口。不可否認,這有很多東西需要消化,而且不是非常直觀。另一方面,你將需要的大部分函數式接口都是爲你編寫的,並且它們的名稱足夠常規,因此在需要時你應該不會遇到太多麻煩。

Most of the standard functional interfaces exist only to provide support for primitive types. Don’t be tempted to use basic functional interfaces with boxed primitives instead of primitive functional interfaces. While it works, it violates the advice of Item 61, “prefer primitive types to boxed primitives.” The performance consequences of using boxed primitives for bulk operations can be deadly.

大多數標準函數式接口的存在只是爲了提供對基本類型的支持。不要嘗試使用帶有包裝類的基本函數式接口,而不是使用基本類型函數式接口。 當它工作時,它違反了 Item-61 的建議,“與盒裝原語相比,更喜歡原語類型”。在批量操作中使用裝箱原語的性能後果可能是致命的。

Now you know that you should typically use standard functional interfaces in preference to writing your own. But when should you write your own? Of course you need to write your own if none of the standard ones does what you need, for example if you require a predicate that takes three parameters, or one that throws a checked exception. But there are times you should write your own functional interface even when one of the standard ones is structurally identical.

現在你知道,與編寫自己的接口相比,通常應該使用標準的函數式接口。但是你應該什麼時候寫你自己的呢?當然,如果標準的函數式接口都不能滿足你的需要,那麼你需要自行編寫,例如,如果你需要一個接受三個參數的 Predicate,或者一個拋出已檢查異常的 Predicate。但是有時候你應該編寫自己的函數接口,即使其中一個標準接口在結構上是相同的。

Consider our old friend Comparator<T>, which is structurally identical to the ToIntBiFunction<T,T> interface. Even if the latter interface had existed when the former was added to the libraries, it would have been wrong to use it. There are several reasons that Comparator deserves its own interface. First, its name provides excellent documentation every time it is used in an API, and it’s used a lot. Second, the Comparator interface has strong requirements on what constitutes a valid instance, which comprise its general contract. By implementing the interface, you are pledging to adhere to its contract. Third, the interface is heavily outfitted with useful default methods to transform and combine comparators.

考慮我們的老朋友 Comparator<T>,它在結構上與 ToIntBiFunction<T,T> 接口相同。即使後者接口在將前者添加到庫時已經存在,使用它也是錯誤的。有幾個原因說明比較器應該有自己的接口。首先,每次在 API 中使用 Comparator 時,它的名稱都提供了優秀的文檔,而且它的使用非常頻繁。通過實現接口,你保證遵守其契約。第三,該接口大量配備了用於轉換和組合比較器的有用默認方法。

You should seriously consider writing a purpose-built functional interface in preference to using a standard one if you need a functional interface that shares one or more of the following characteristics with Comparator:

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

  • It will be commonly used and could benefit from a descriptive name.

它將被廣泛使用,並且可以從描述性名稱中獲益。

  • It has a strong contract associated with it.

它有一個強有力的約定。

  • It would benefit from custom default methods.

它將受益於自定義默認方法。

If you elect to write your own functional interface, remember that it’s an interface and hence should be designed with great care (Item 21).

如果你選擇編寫自己的函數式接口,請記住這是一個接口,因此應該非常小心地設計它(Item-21)。

Notice that the EldestEntryRemovalFunction interface (page 199) is labeled with the @FunctionalInterface annotation. This annotation type is similar in spirit to @Override. It is a statement of programmer intent that serves three purposes: it tells readers of the class and its documentation that the interface was designed to enable lambdas; it keeps you honest because the interface won’t compile unless it has exactly one abstract method; and it prevents maintainers from accidentally adding abstract methods to the interface as it evolves. Always annotate your functional interfaces with the @FunctionalInterface annotation.

注意 EldestEntryRemovalFunction 接口(第199頁)使用 @FunctionalInterface 註釋進行標記。這種註釋類型在本質上類似於 @Override。它是程序員意圖的聲明,有三個目的:它告訴類及其文檔的讀者,接口的設計是爲了啓用 lambda 表達式;它使你保持誠實,因爲接口不會編譯,除非它只有一個抽象方法;它還可以防止維護者在接口發展過程中意外地向接口添加抽象方法。總是用 @FunctionalInterface 註釋你的函數接口。

A final point should be made concerning the use of functional interfaces in APIs. Do not provide a method with multiple overloadings that take different functional interfaces in the same argument position if it could create a possible ambiguity in the client. This is not just a theoretical problem. The submit method of ExecutorService can take either a Callable<T> or a Runnable, and it is possible to write a client program that requires a cast to indicate the correct overloading (Item 52). The easiest way to avoid this problem is not to write overloadings that take different functional interfaces in the same argument position. This is a special case of the advice in Item 52, “use overloading judiciously.”

最後一點應該是關於 API 中函數式接口的使用。不要提供具有多個重載的方法,這些方法採用相同參數位置的不同函數式接口,否則會在客戶機中造成可能的歧義。這不僅僅是一個理論問題。ExecutorService 的 submit 方法可以是 Callable<T> 級的,也可以是 Runnable 的,並且可以編寫一個客戶端程序,它需要一個類型轉換來指示正確的重載(Item 52)。避免此問題的最簡單方法是不要編寫將不同函數式接口放在相同參數位置的重載。這是 Item-52 「明智地使用過載」建議的一個特例。

In summary, now that Java has lambdas, it is imperative that you design your APIs with lambdas in mind. Accept functional interface types on input and return them on output. It is generally best to use the standard interfaces provided in java.util.function.Function, but keep your eyes open for the relatively rare cases where you would be better off writing your own functional interface.

總之,既然 Java 已經有了 lambda 表達式,你必須在設計 API 時考慮 lambda 表達式。在輸入時接受函數式接口類型,在輸出時返回它們。一般情況下,最好使用 java.util.function 中提供的標準函數式接口,但請注意比較少見的一些情況,在這種情況下,你最好編寫自己的函數式接口。


Back to contents of the chapter(返回章節目錄)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章