Java 8 習慣用語: 級聯 lambda 表達式

Jogmcqaa 7年前發布 | 39K 次閱讀 Java Java開發 Lambda

Java 8 習慣用語

級聯 lambda 表達式

可重用的函數有助于讓代碼變得非常簡短,但是會不會過于簡短呢?

Venkat Subramaniam

2017 年 11 月 29 日發布

系列內容:

此內容是該系列 9 部分中的第 # 部分: Java 8 習慣用語

https://www.ibm.com/developerworks/cn/library/?series_title_by=**auto**

敬請期待該系列的后續內容。

此內容是該系列的一部分: Java 8 習慣用語

敬請期待該系列的后續內容。

關于本系列

Java 8 是自 Java 語言誕生以來進行的一次最重大更新 — 包含了非常豐富的新功能,您可能想知道從何處開始著手了解它。在本系列中,作家兼教師 Venkat Subramaniam 提供了一種慣用的 Java 8 編程方法:這些簡短的探索會激發您反思您認為理所當然的 Java 約定,同時逐步將新技術和語法集成到您的程序中。

在函數式編程中,函數既可以接收也可以返回其他函數。函數不再像傳統的面向對象編程中一樣,只是一個對象的 工廠生成器 ,它也能夠創建和返回另一個函數。返回函數的函數可以變成 級聯 lambda 表達式 ,特別值得注意的是代碼非常簡短。盡管此語法初看起來可能非常陌生,但它有自己的用途。本文將幫助您認識級聯 lambda 表達式,理解它們的性質和在代碼中的用途。

神秘的語法

您是否看到過類似這樣的代碼段?

x -> y -> x > y

如果您很好奇“這到底是什么意思?”,那么您并不孤單。對于不熟悉使用 lambda 表達式編程的開發人員,此語法可能看起來像貨物正從快速行駛的卡車上一件件掉下來一樣。

幸運的是,我們不會經常看到它們,但理解如何創建級聯 lambda 表達式和如何在代碼中理解它們會大大減少您的受挫感。

高階函數

在談論級聯 lambda 表達式之前,有必要首先理解如何創建它們。對此,我們需要回顧一下高階函數(已在本系列第 1 篇文章中介紹)和它們在 函數分解 中的作用,函數分解是一種將復雜流程分解為更小、更簡單的部分的方式。

首先,考慮區分高階函數與常規函數的規則:

常規函數

  • 可以接收對象
  • 可以創建對象
  • 可以返回對象

高階函數

  • 可以接收函數
  • 可以創建函數
  • 可以返回函數

開發人員將匿名函數或 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 表達式的引用,該表達式在調用時返回 另一個 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 約定來減少雜亂,讓代碼更簡短。

我們首先重構以下代碼:

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 表達式,而不是接受一個操作或返回一個值時,您將看到兩個箭頭。這種代碼非常簡短,但可能在最初遇到時非常難以理解。但是,一旦您學會識別這種函數式語法,理解和掌握它就會變得容易得多。

 

來自:http://www.ibm.com/developerworks/cn/java/j-java8idioms9/index.html?ca=drs-

 

 本文由用戶 Jogmcqaa 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!