Java8 新語法習慣 (級聯 lambda 表示式)

我是傳奇哈哈發表於2019-02-26

在函數語言程式設計中,函式既可以接收也可以返回其他函式。函式不在像傳統的物件導向程式設計一樣,只是一個物件的工廠或生成器,它也能夠建立和返回另一個函式。返回函式的函式可以變成級聯 lambda 表示式,程式碼非常短。儘管這樣的語法初次看起來非常的陌生,但是它有自己的用途。本文將幫助您認識級聯 lambda 表示式,理解它們的性質和在程式碼中的用途。

神祕的語法

看下面的一端程式碼:

x -> y -> x > y
複製程式碼

對於不熟悉使用 lambda 表示式程式設計的開發人員,此語法可能看起來像貨物正在從快速行駛的卡車上一件件掉下來。這非常的令人感到好奇。幸運的是我們不會經常看到他們,但理解如何建立級聯 lambda 表示式和如何在程式碼中理解它們會是很有好處的。

高階函式

在談論級聯 lambda 表示式之前,有必要首先理解如何建立它們。對此,我們需要回顧一下高階函式和它們在函式分解中的作用,函式分解是一種將複雜流程分解為更小、更簡單的部分的方式。

首先區分一下高階函式和常規函式的規則:

常規函式

  • 可以接收物件
  • 可以建立物件
  • 可以返回物件

高階函式

  • 可以接收函式
  • 可以建立函式
  • 可以返回函式

開發人員可以將匿名函式或 lambda 表示式傳遞給高階函式,以讓程式碼簡短且富於表達。讓我們看看這個高階函式的兩個示例。

一個接收函式的函式

在 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);
複製程式碼

作為規則,只要一個函式介面顯示為一個函式的引數的型別,您看到的就是一個高階函式

一個返回函式的函式

函式可以接收函式、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);
複製程式碼

建立可重用的函式

現在已經大體瞭解高階函式和如何在程式碼中找到他們,我們可以考慮使用他們來讓程式碼更加簡短。

設想我們有兩個列表 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 表示式的引用,該表示式在呼叫時返回另一個 lambda 表示式 — 換言之,這裡隱藏著一種 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 約定來減少雜亂,讓程式碼更簡短。

可以使用型別引用來從外部和內部 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 表示式,而不是接受一個操作或返回一個值時,您將看到兩個箭頭。

感謝 Venkat Subramaniam 博士

Venkat Subramaniam 博士站點:http://agiledeveloper.com/
知識改變生活,努力改變生活

Java8 新語法習慣 (級聯 lambda 表示式)

相關文章