在函數式程式設計中,函數既可以接收也可以傳回其他函數。函數不再像傳統的面向對象程式設計中一樣,隻是一個對象的工廠或生成器,它也能夠建立和傳回另一個函數。傳回函數的函數可以變成級聯 lambda 表達式,特别值得注意的是代碼非常簡短。盡管此文法初看起來可能非常陌生,但它有自己的用途。本文将幫助您認識級聯 lambda 表達式,了解它們的性質和在代碼中的用途。
神秘的文法
您是否看到過類似這樣的代碼段?
x -> y -> x > y
如果您很好奇“這到底是什麼意思?”,那麼您并不孤單。對于不熟悉使用 lambda 表達式程式設計的開發人員,此文法可能看起來像貨物正從快速行駛的卡車上一件件掉下來一樣。
幸運的是,我們不會經常看到它們,但了解如何建立級聯 lambda 表達式和如何在代碼中了解它們會大大減少您的受挫感。
高階函數
在談論級聯 lambda 表達式之前,有必要首先了解如何建立它們。對此,我們需要回顧一下高階函數和它們在函數分解中的作用,函數分解是一種将複雜流程分解為更小、更簡單的部分的方式。
首先,考慮區分高階函數與正常函數的規則:
正常函數
- 可以接收對象
- 可以建立對象
- 可以傳回對象
高階函數
- 可以接收函數
- 可以建立函數
- 可以傳回函數
開發人員将匿名函數或 lambda 表達式傳遞給高階函數,以讓代碼簡短且富于表達。讓我們看看這些高階函數的兩個示例。
示例 1:一個接收函數的函數
在 Java™ 中,我們使用函數接口來引用 lambda 表達式和方法引用。下面這個函數接收一個對象和一個函數:
public static int totalSelectedValues(List<Integer> values,
Predicate<Integer> selector) {
return values.stream()
.filter(selector)
.reduce(0, Integer::sum);
}
totalSelectedValues
的第一個參數是集合對象,而第二個參數是
Predicate
函數接口。 因為參數類型是函數接口 (
Predicate
),是以我們現在可以将一個 lambda 表達式作為第二個參數傳遞給
totalSelectedValues
。例如,如果我們想僅對一個
numbers
清單中的偶數值求和,可以調用
totalSelectedValues
,如下所示:
totalSelectedValues(numbers, e -> e % 2 == 0);
假設我們現在在
Util
類中有一個名為
isEven
的
static
方法。在此情況下,我們可以使用
isEven
作為
totalSelectedValues
的參數,而不傳遞 lambda 表達式:
totalSelectedValues(numbers, Util::isEven);
作為規則,隻要一個函數接口顯示為一個函數的參數的類型,您看到的就是一個高階函數。
示例 2:一個傳回函數的函數
函數可以接收函數、lambda 表達式或方法引用作為參數。同樣地,函數也可以傳回 lambda 表達式或方法引用。在此情況下,傳回類型将是函數接口。
讓我們首先看一個建立并傳回
Predicate
來驗證給定值是否為奇數的函數:
public static Predicate<Integer> createIsOdd() {
Predicate<Integer> check = (Integer number) -> number % 2 != 0;
return check;
}
為了傳回一個函數,我們必須提供一個函數接口作為傳回類型。在本例中,我們的函數接口是
Predicate
。盡管上述代碼在文法上是正确的,但它可以更加簡短。 我們使用類型引用并删除臨時變量來改進該代碼:
public static Predicate<Integer> createIsOdd() {
return number -> number % 2 != 0;
}
這是使用的
createIsOdd
方法的一個示例:
Predicate<Integer> isOdd = createIsOdd();
isOdd.test(4);
請注意,在
isOdd
上調用
test
會傳回
false
。我們也可以在
isOdd
上使用更多值來調用
test
;它并不限于使用一次。
建立可重用的函數
現在您已大體了解高階函數和如何在代碼中找到它們,我們可以考慮使用它們來讓代碼更加簡短。
設想我們有兩個清單
numbers1
和
numbers2
。假設我們想從第一個清單中僅提取大于 50 的數,然後從第二個清單中提取大于 50 的值并乘以 2。
可通過以下代碼實作這些目的:
List<Integer> result1 = numbers1.stream()
.filter(e -> e > 50)
.collect(toList());
List<Integer> result2 = numbers2.stream()
.filter(e -> e > 50)
.map(e -> e * 2)
.collect(toList());
此代碼很好,但您注意到它很冗長了嗎?我們對檢查數字是否大于 50 的 lambda 表達式使用了兩次。 我們可以通過建立并重用一個
Predicate
,進而删除重複代碼,讓代碼更富于表達:
Predicate<Integer> isGreaterThan50 = number -> number > 50;
List<Integer> result1 = numbers1.stream()
.filter(isGreaterThan50)
.collect(toList());
List<Integer> result2 = numbers2.stream()
.filter(isGreaterThan50)
.map(e -> e * 2)
.collect(toList());
通過将 lambda 表達式存儲在一個引用中,我們可以重用它,這是我們避免重複 lambda 表達式的方式。如果我們想跨方法重用 lambda 表達式,也可以将該引用放入一個單獨的方法中,而不是放在一個局部變量引用中。
現在假設我們想從清單
numbers1
中提取大于 25、50 和 75 的值。我們可以首先編寫 3 個不同的 lambda 表達式:
List<Integer> valuesOver25 = numbers1.stream()
.filter(e -> e > 25)
.collect(toList());
List<Integer> valuesOver50 = numbers1.stream()
.filter(e -> e > 50)
.collect(toList());
List<Integer> valuesOver75 = numbers1.stream()
.filter(e -> e > 75)
.collect(toList());
盡管上面每個 lambda 表達式将輸入與一個不同的值比較,但它們做的事情完全相同。如何以較少的重複來重寫此代碼?
建立和重用 lambda 表達式
盡管上一個示例中的兩個 lambda 表達式相同,但上面 3 個表達式稍微不同。建立一個傳回
Predicate
的
Function
可以解決此問題。
首先,函數接口
Function<T, U>
将一個
T
類型的輸入轉換為
U
類型的輸出。例如,下面的示例将一個給定值轉換為它的平方根:
Function<Integer, Double> sqrt = value -> Math.sqrt(value);
在這裡,傳回類型
U
可以很簡單,比如
Double
、
String
或
Person
。或者它也可以更複雜,比如
Consumer
或
Predicate
等另一個函數接口。
在本例中,我們希望一個
Function
建立一個
Predicate
。是以代碼如下:
Function<Integer, Predicate<Integer>> isGreaterThan = (Integer pivot) -> {
Predicate<Integer> isGreaterThanPivot = (Integer candidate) -> {
return candidate > pivot;
};
return isGreaterThanPivot;
};
引用
isGreaterThan
引用了一個表示
Function<T, U>
— 或更準确地講表示
Function<Integer, Predicate<Integer>>
的 lambda 表達式。輸入是一個
Integer
,輸出是一個
Predicate<Integer>
。
在 lambda 表達式的主體中(外部
{}
内),我們建立了另一個引用
isGreaterThanPivot
,它包含對另一個 lambda 表達式的引用。這一次,該引用是一個
Predicate
而不是
Function
。最後,我們傳回該引用。
isGreaterThan
是一個 lambda 表達式的引用,該表達式在調用時傳回另一個
現在,我們可以使用新建立的外部 lamba 表達式來解決代碼中的重複問題:
List<Integer> valuesOver25 = numbers1.stream()
.filter(isGreaterThan.apply(25))
.collect(toList());
List<Integer> valuesOver50 = numbers1.stream()
.filter(isGreaterThan.apply(50))
.collect(toList());
List<Integer> valuesOver75 = numbers1.stream()
.filter(isGreaterThan.apply(75))
.collect(toList());
在
isGreaterThan
上調用
apply
會傳回一個
Predicate
,後者然後作為參數傳遞給
filter
方法。
盡管整個過程非常簡單(作為示例),但是能夠抽象為一個函數對于謂詞更加複雜的場景來說尤其有用。
保持簡短的秘訣
我們已從代碼中成功删除了重複的 lambda 表達式,但
isGreaterThan
的定義看起來仍然很雜亂。幸運的是,我們可以組合一些 Java 8 約定來減少雜亂,讓代碼更簡短。
我們首先重構以下代碼:
Function<Integer, Predicate<Integer>> isGreaterThan = (Integer pivot) -> {
Predicate<Integer> isGreaterThanPivot = (Integer candidate) -> {
return candidate > pivot;
};
return isGreaterThanPivot;
};
可以使用類型引用來從外部和内部 lambda 表達式的參數中删除類型細節:
Function<Integer, Predicate<Integer>> isGreaterThan = (pivot) -> {
Predicate<Integer> isGreaterThanPivot = (candidate) -> {
return candidate > pivot;
};
return isGreaterThanPivot;
};
目前,我們從代碼中删除了兩個單詞,改進不大。
接下來,我們删除多餘的
()
,以及外部 lambda 表達式中不必要的臨時引用:
Function<Integer, Predicate<Integer>> isGreaterThan = pivot -> {
return candidate -> {
return candidate > pivot;
};
};
代碼更加簡短了,但是仍然看起來有些雜亂。
可以看到内部 lambda 表達式的主體隻有一行,顯然
{}
和
return
是多餘的。讓我們删除它們:
Function<Integer, Predicate<Integer>> isGreaterThan = pivot -> {
return candidate -> candidate > pivot;
};
現在可以看到,外部 lambda 表達式的主體也隻有一行,是以
{}
和
return
在這裡也是多餘的。在這裡,我們應用最後一次重構:
Function<Integer, Predicate<Integer>> isGreaterThan =
pivot -> candidate -> candidate > pivot;
現在可以看到 — 這是我們的級聯 lambda 表達式。
了解級聯 lambda 表達式
我們通過一個适合每個階段的重構過程,得到了最終的代碼 - 級聯 lambda 表達式。在本例中,外部 lambda 表達式接收
pivot
作為參數,内部 lambda 表達式接收
candidate
作為參數。内部 lambda 表達式的主體同時使用它收到的參數 (
candidate
) 和來自外部範圍的參數。也就是說,内部 lambda 表達式的主體同時依靠它的參數和它的詞法範圍或定義範圍。
級聯 lambda 表達式對于編寫它的人非常有意義。但是對于讀者呢?
看到一個隻有一個向右箭頭 (
->
) 的 lambda 表達式時,您應該知道您看到的是一個匿名函數,它接受參數(可能是空的)并執行一個操作或傳回一個結果值。
看到一個包含兩個向右箭頭 (
->
) 的 lambda 表達式時,您看到的也是一個匿名函數,但它接受參數(可能是空的)并傳回另一個 lambda 表達式。傳回的 lambda 表達式可以接受它自己的參數或者可能是空的。它可以執行一個操作或傳回一個值。它甚至可以傳回另一個 lambda 表達式,但這通常有點大材小用,最好避免。
大體上講,當您看到兩個向右箭頭時,可以将第一個箭頭右側的所有内容視為一個黑盒:一個由外部 lambda 表達式傳回的 lambda 表達式。
結束語
級聯 lambda 表達式不是很常見,但您應該知道如何在代碼中識别和了解它們。當一個 lambda 表達式傳回另一個 lambda 表達式,而不是接受一個操作或傳回一個值時,您将看到兩個箭頭。這種代碼非常簡短,但可能在最初遇到時非常難以了解。但是,一旦您學會識别這種函數式文法,了解和掌握它就會變得容易得多。
原作者:Venkat Subramaniam