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

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

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

神祕的語法

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

x -> y -> x > y

若是您很好奇「這究竟是什麼意思?」,那麼您並不孤單。對於不熟悉使用 lambda 表達式編程的開發人員,此語法可能看起來像貨物正從快速行駛的卡車上一件件掉下來同樣。編程

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

高階函數

在談論級聯 lambda 表達式以前,有必要首先理解如何建立它們。對此,咱們須要回顧一下高階函數和它們在函數分解中的做用,函數分解是一種將複雜流程分解爲更小、更簡單的部分的方式。ide

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

常規函數函數

  • 能夠接收對象
  • 能夠建立對象
  • 能夠返回對象

高階函數code

  • 能夠接收函數
  • 能夠建立函數
  • 能夠返回函數

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

示例 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 類中有一個名爲 isEvenstatic 方法。在此狀況下,咱們可使用 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;它並不限於使用一次。

建立可重用的函數

如今您已大致瞭解高階函數和如何在代碼中找到它們,咱們能夠考慮使用它們來讓代碼更加簡短。

設想咱們有兩個列表 numbers1numbers2。假設咱們想從第一個列表中僅提取大於 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 中提取大於 2五、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 個表達式稍微不一樣。建立一個返回 PredicateFunction 能夠解決此問題。

首先,函數接口 Function<T, U> 將一個 T 類型的輸入轉換爲 U 類型的輸出。例如,下面的示例將一個給定值轉換爲它的平方根:

Function<Integer, Double> sqrt = value -> Math.sqrt(value);

在這裏,返回類型 U 能夠很簡單,好比 DoubleStringPerson。或者它也能夠更復雜,好比 ConsumerPredicate 等另外一個函數接口。

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

原做者:Venkat Subramaniam  
原文連接: Java 8 習慣用語
原出處: IBM Developer

e3f6982e97de58e289858a0ec142affe.jpeg

相關文章
相關標籤/搜索