Java 8中的功能接口

1.簡介

本文是Java 8中存在的不同功能接口的指南,它們的一般使用情況以及標準JDK庫中的用法。

2. Java 8中的Lambda

Java 8以lambda表達式的形式帶來了強大的新語法改進。 Lambda是一個匿名函數,可以作為一流語言的值來處理,例如傳遞給方法或從方法返回。

在Java 8之前,通常會針對需要封裝單個功能的每種情況創建一個類。這暗示了很多不必要的樣板代碼來定義用作原始函數表示的內容。

本指南重點介紹java.util.function軟件包中提供的某些特定功能接口。

3.功能接口

建議所有功能接口都具有豐富的@FunctionalInterface批註。這不僅清楚地傳達了此接口的用途,而且如果帶註釋的接口不滿足條件,還允許編譯器生成錯誤。

具有SAM(單一抽象方法)的任何接口都是功能性接口,並且其實現可以視為lambda表達式。

請注意,Java 8的default方法不是abstract ,也不算在內:功能接口可能仍具有多個default方法。您可以通過查看Function's文檔來觀察此情況。

4.功能

Lambda最簡單,最通用的情況是一個函數接口,該函數接口的方法接收一個值並返回另一個值。單個參數的此功能由Function接口表示,該接口由其參數的類型和返回值進行參數化:

public interface Function<T, R> { … }

標準庫中Function類型的用法之一是Map.computeIfAbsent方法,該方法按鍵從映射返回值,但如果映射中不存在鍵,則計算一個值。要計算一個值,它使用傳遞的Function實現:

Map<String, Integer> nameMap = new HashMap<>();

 Integer value = nameMap.computeIfAbsent("John", s -> s.length());

在這種情況下,將通過對鍵應用函數來計算值,然後將其放在映射中並從方法調用中返回。順便說一句,我們可以用匹配傳遞和返回值類型的方法引用替換lambda

請記住,在其上調用方法的對象實際上是方法的隱式第一個參數,它允許將實例方法的length引用強制轉換為Function接口:

Integer value = nameMap.computeIfAbsent("John", String::length);

Function接口還具有默認的compose方法,該方法允許將多個函數組合為一個函數並按順序執行它們:

Function<Integer, String> intToString = Object::toString;

 Function<String, String> quote = s -> "'" + s + "'";



 Function<Integer, String> quoteIntToString = quote.compose(intToString);



 assertEquals("'5'", quoteIntToString.apply(5));

quoteIntToString函數是quote函數應用於intToString函數結果的intToString

5.原始函數專業化

由於原始類型不能是泛型類型參數,因此對於大多數使用的原始類型doubleintlong和它們在參數和返回類型中的組合,有Function接口的版本:

  • IntFunctionLongFunctionDoubleFunction:參數為指定類型,返回類型為參數化
  • ToIntFunctionToLongFunctionToDoubleFunction:返回類型為指定類型,參數已參數化
  • DoubleToIntFunctionDoubleToLongFunctionIntToDoubleFunctionIntToLongFunctionLongToIntFunctionLongToDoubleFunction -將參數和返回類型均定義為原始類型,如其名稱所指定

有沒有外的現成功能接口,比如說,這需要一個功能short ,並返回一個byte ,但沒有阻止你寫你自己:

@FunctionalInterface

 public interface ShortToByteFunction {



 byte applyAsByte(short s);



 }

現在我們可以編寫一個方法,該方法使用ShortToByteFunction定義的規則將short數組轉換為byte數組:

public byte[] transformArray(short[] array, ShortToByteFunction function) {

 byte[] transformedArray = new byte[array.length];

 for (int i = 0; i < array.length; i++) {

 transformedArray[i] = function.applyAsByte(array[i]);

 }

 return transformedArray;

 }

這是我們如何使用它將短褲數組轉換為字節數組乘以2的方法:

short[] array = {(short) 1, (short) 2, (short) 3};

 byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));



 byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};

 assertArrayEquals(expectedArray, transformedArray);

6.兩元函數專業化

要使用兩個參數定義lambda,我們必須使用其他名稱中包含“ Bi”關鍵字的接口: BiFunctionToDoubleBiFunctionToIntBiFunctionToLongBiFunction

BiFunction具有參數和通用的返回類型,而ToDoubleBiFunction和其他ToDoubleBiFunction則允許您返回原始值。

在標準API中使用此接口的典型示例之一是Map.replaceAll方法,該方法允許將地圖中的所有值替換為某些計算值。

讓我們使用一個BiFunction實現,該實現接收一個鍵和一個舊值,以計算薪水的新值並將其返回。

Map<String, Integer> salaries = new HashMap<>();

 salaries.put("John", 40000);

 salaries.put("Freddy", 30000);

 salaries.put("Samuel", 50000);



 salaries.replaceAll((name, oldValue) ->

 name.equals("Freddy") ? oldValue : oldValue + 10000);

7.供應商

Supplier功能接口是又一個不帶任何參數的Function專業化。它通常用於延遲生成值。例如,讓我們定義一個將double值平方的函數。它本身將不會收到任何值,而是會收到以下值的Supplier

public double squareLazy(Supplier<Double> lazyValue) {

 return Math.pow(lazyValue.get(), 2);

 }

這使我們可以使用Supplier實現來延遲生成用於調用此函數的參數。如果此參數的生成花費大量時間,則這可能很有用。我們將使用番石榴的sleepUninterruptibly方法進行模擬:

Supplier<Double> lazyValue = () -> {

 Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);

 return 9d;

 };



 Double valueSquared = squareLazy(lazyValue);

供應商的另一個用例是定義用於序列生成的邏輯。為了演示它,讓我們使用靜態Stream.generate方法創建斐波那契數字Stream

int[] fibs = {0, 1};

 Stream<Integer> fibonacci = Stream.generate(() -> {

 int result = fibs[1];

 int fib3 = fibs[0] + fibs[1];

 fibs[0] = fibs[1];

 fibs[1] = fib3;

 return result;

 });

傳遞給Stream.generate方法的函數實現了Supplier函數接口。請注意,要用作生成器, Supplier通常需要某種外部狀態。在這種情況下,其狀態由兩個最後的斐波那契序列號組成。

為了實現此狀態,我們使用數組而不是幾個變量,因為在lambda內部使用的所有外部變量必須有效地是final

Supplier功能接口的其他特殊功能包括BooleanSupplierDoubleSupplierLongSupplierIntSupplier ,它們的返回類型是對應的原語。

8.消費者

Supplier相反, Consumer接受泛化的論點並且不返回任何內容。它是代表副作用的功能。

例如,讓我們通過在控制台中打印問候語來在名稱列表中向所有人打招呼。傳遞給List.forEach方法的lambda實現Consumer函數接口:

List<String> names = Arrays.asList("John", "Freddy", "Samuel");

 names.forEach(name -> System.out.println("Hello, " + name));

還有一些專用的Consumer版本— DoubleConsumerIntConsumerLongConsumer ,它們接收原始值作為參數。更有趣的是BiConsumer界面。它的用例之一是遍歷映射的條目:

Map<String, Integer> ages = new HashMap<>();

 ages.put("John", 25);

 ages.put("Freddy", 24);

 ages.put("Samuel", 30);



 ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

另一套專門的BiConsumer版本集由ObjDoubleConsumerObjIntConsumerObjLongConsumer ,它們接收兩個自變量,其中一個被通用化,而另一個則是原始類型。

9.謂詞Predicates

在數學邏輯中,謂詞是一個接收值並返回布爾值的函數。

Predicate功能接口是接收通用值並返回布爾值的Function一種特殊形式。 Predicate lambda的典型用例是過濾值的集合:

List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");



 List<String> namesWithA = names.stream()

 .filter(name -> name.startsWith("A"))

 .collect(Collectors.toList());

在上面的代碼中,我們使用Stream API過濾列表,僅保留以字母“ A”開頭的名稱。過濾邏輯封裝在Predicate實現中。

與前面的所有示例一樣,此函數具有接收原始值的IntPredicateDoublePredicateLongPredicate版本。

10.運算符

Operator接口是函數的特殊情況,它們接收並返回相同的值類型。 UnaryOperator接口接收單個參數。在Collections API中,其用例之一是將列表中的所有值替換為某些相同類型的計算值:

List<String> names = Arrays.asList("bob", "josh", "megan");



 names.replaceAll(name -> name.toUpperCase());

List.replaceAll函數返回void ,因為它替換了適當的值。為了達到此目的,用於轉換列表值的lambda必須返回與接收到的結果類型相同的結果。這就是為什麼UnaryOperator在這裡有用的原因。

當然, name -> name.toUpperCase() ,您還可以簡單地使用方法引用:

names.replaceAll(String::toUpperCase);

BinaryOperator的最有趣的用例之一是歸約運算。假設我們要聚合所有值之和中的整數集合。使用Stream API,我們可以使用Collector做到這一點,但是更通用的方法是使用reduce方法:

List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);



 int sum = values.stream()

 .reduce(0, (i1, i2) -> i1 + i2);

reduce方法接收初始累加器值和BinaryOperator函數。該函數的參數是一對相同類型的值,並且函數本身包含用於將它們連接到相同類型的單個值中的邏輯。傳遞的函數必須是associative ,這意味著值聚合的順序無關緊要,即應滿足以下條件:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

BinaryOperator運算符函數的關聯屬性允許輕鬆並行化約簡過程。

當然, UnaryOperatorBinaryOperator也有可以與原始值一起使用的特化,即DoubleUnaryOperatorIntUnaryOperatorLongUnaryOperatorDoubleBinaryOperatorIntBinaryOperatorLongBinaryOperator

11.舊版功能接口

並非所有功能接口都出現在Java 8中。早期Java版本中的許多接口都符合FunctionalInterface的約束,可以用作lambda。一個突出的例子是並發API中使用的RunnableCallable接口。在Java 8中,這些接口還標有@FunctionalInterface批註。這使我們可以大大簡化並發代碼:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));

 thread.start();

12.結論

在本文中,我們描述了Java 8 API中存在的可用作lambda表達式的不同功能接口。這篇文章的源代碼可以在GitHub上找到