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.原始函數專業化
由於原始類型不能是泛型類型參數,因此對於大多數使用的原始類型double
, int
, long
和它們在參數和返回類型中的組合,有Function
接口的版本:
-
IntFunction
,LongFunction
,DoubleFunction:
參數為指定類型,返回類型為參數化 -
ToIntFunction
,ToLongFunction
,ToDoubleFunction:
返回類型為指定類型,參數已參數化 -
DoubleToIntFunction
,DoubleToLongFunction
,IntToDoubleFunction
,IntToLongFunction
,LongToIntFunction
,LongToDoubleFunction
-將參數和返回類型均定義為原始類型,如其名稱所指定
有沒有外的現成功能接口,比如說,這需要一個功能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”
關鍵字的接口: BiFunction
, ToDoubleBiFunction
, ToIntBiFunction
和ToLongBiFunction
。
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
功能接口的其他特殊功能包括BooleanSupplier
, DoubleSupplier
, LongSupplier
和IntSupplier
,它們的返回類型是對應的原語。
8.消費者
與Supplier
相反, Consumer
接受泛化的論點並且不返回任何內容。它是代表副作用的功能。
例如,讓我們通過在控制台中打印問候語來在名稱列表中向所有人打招呼。傳遞給List.forEach
方法的lambda實現Consumer
函數接口:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));
還有一些專用的Consumer
版本— DoubleConsumer
, IntConsumer
和LongConsumer
,它們接收原始值作為參數。更有趣的是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
版本集由ObjDoubleConsumer
, ObjIntConsumer
和ObjLongConsumer
,它們接收兩個自變量,其中一個被通用化,而另一個則是原始類型。
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
實現中。
與前面的所有示例一樣,此函數具有接收原始值的IntPredicate
, DoublePredicate
和LongPredicate
版本。
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
運算符函數的關聯屬性允許輕鬆並行化約簡過程。
當然, UnaryOperator
和BinaryOperator
也有可以與原始值一起使用的特化,即DoubleUnaryOperator
, IntUnaryOperator
, LongUnaryOperator
, DoubleBinaryOperator
, IntBinaryOperator
和LongBinaryOperator
。
11.舊版功能接口
並非所有功能接口都出現在Java 8中。早期Java版本中的許多接口都符合FunctionalInterface
的約束,可以用作lambda。一個突出的例子是並發API中使用的Runnable
和Callable
接口。在Java 8中,這些接口還標有@FunctionalInterface
批註。這使我們可以大大簡化並發代碼:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();
12.結論
在本文中,我們描述了Java 8 API中存在的可用作lambda表達式的不同功能接口。這篇文章的源代碼可以在GitHub上找到。