【Java8實戰】Lambda表達式(一)


Java 8的Lambda表達式借鑒了C#和Scala等語言中的類似特性,簡化了匿名函數的表達方式。Lambda表達式可以直接以內聯的形式為函數式接口的抽象方法提供實現,並把整個表達式作為函數式接口的實例。什么是函數式接口?簡單來說就是只包含一個抽象方法的接口,允許有默認的實現(使用default關鍵字描述方法)。函數式接口建議使用@FunctionalInterface注解標注,雖然這不是必須的,但是這樣做更符合規范。

 

在Java 8之前,實現Runnable常用方式是編寫一個匿名類:

Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
      System.out.println("hello");
  }
});
thread.start();

 

使用Lambda表達式后,上面的代碼可以改造為:

Thread thread = new Thread(() -> System.out.println("hello"));
thread.start();

 

是不是很神奇?!很簡潔?!

Lambda表達式解析

Lambda表達式的基本語法如下:

(parameters) -> expression 
or
(parameters) -> { statements; }

 

由語法可以看到,Lambda表達式包含了三個部分:

  • 參數列表;

  • 箭頭->把參數列表與Lambda主體分隔開;

  • Lambda主體,只有一行代碼的時候可以省略大括號和return關鍵字。

比如下面這些Lambda表達式都是合法的:

(String str) -> str.length()
(String str) -> { return str.length(); }

() -> System.out.println("hello")

() -> {}
() -> 17

(int x, int y) -> {
  System.out.println(x);
  System.out.println(y);
}

 

Lambda的使用場合

什么時候可以使用Lambda表達式?使用Lambda必須滿足以下兩個條件:

  1. 實現的對象是函數式接口的抽象方法;

  2. 函數式接口的抽象方法的函數描述符和Lambda表達式的函數描述符一致。

函數式接口

函數式接口的定義開頭已經說了,這里就不再贅述。在Java 8之前,常見的函數式接口有java.util.Comparatorjava.lang.Runnable等。拿java.util.Runnable來說,查看其源碼如下:

@FunctionalInterface
public interface Runnable {
  public abstract void run();
}

 

這個接口只有一個抽象方法,並且使用@FunctionalInterface注解標注。

接口現在還可以擁有默認方法(即在類沒有對方法進行實現時,其主體為方法提供默認實現的方法)。哪怕有很多默認方法,只要接口只定義了一個抽象方法,它就仍然是一個函數式接口。

函數描述符

函數描述符其實也可以理解為方法的簽名。比如上述的Runnable抽象方法不接受參數,並且返回void,所以其函數描述符為() -> void。而() -> System.out.println("hello")Lambda表達式也是不接受參數,並且返回void,即其函數描述符也是() -> void。所以代碼Runnable r = () -> System.out.println("hello");是合法的。

特殊的void兼容規則

如果一個Lambda的主體是一個語句表達式, 它就和一個返回void的函數描述符兼容(當然需要參數列表也兼容)。例如,以下Lambda是合法的,盡管List的add方法返回了一個 boolean,而不是Runnable抽象方法函數描述符() -> void所要求的void:

List<String> list = new ArrayList<>();
Runnable r = () -> list.add("hello");

更簡潔的Lambda

編寫一個類型轉換的函數式接口:

@FunctionalInterface
public interface TransForm<T, R> {
  R transForm(T t);
}

 

編寫一個Lambda表達式實現該函數式接口,用於實現String轉換為Integer,代碼如下:

TransForm<String, Integer> t = (String str) -> Integer.valueOf(str);
System.out.println(t.transForm("123"));

 

上面的Lambda表達式可以進一步簡化為如下方式:

TransForm<String, Integer> t = (str) -> Integer.valueOf(str);
System.out.println(t.transForm("123"));

 

因為Java編譯器會從上下文(目標類型)推斷出用什么函數式接口來配合Lambda表達式,這意味着它也可以推斷出適合Lambda的簽名。就拿這個例子來說,TransForm的抽象方法transForm在本例中的函數描述符為(String) -> Integer,所以對應的Lambda的簽名也是如此,即Lambda的參數即使不聲名類型,Java編譯器可以知道其參數實際上為String類型。

其實,上面的Labmda表達式還不是最簡潔的,其還可以更進一步地簡化為如下寫法:

TransForm<String, Integer> t = Integer::valueOf;
System.out.println(t.transForm("123"));

 

你肯定很困惑,這還是Lambda表達式嗎,箭頭去哪里了?雙冒號又是什么鬼?其實這種寫法有一個新的名稱,叫做方法的引用

方法引用可以被看作僅僅調用特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個Lambda代表的只是“直接調用這個方法”,那最好還是用名稱來調用它,而不是去描述如何調用它,這樣代碼可讀性更好。基本寫法就是目標引用放在分隔符::前,方法的名稱放在后面。

舉幾個Lambda及其等效方法引用的例子:

Lambda表達式 等效方法引用
(String s) -> System.out.println(s) System.out::println
(str, i) -> str.substring(i) String::substring
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack

符號::除了出現在方法的引用外,它還常見於構造函數的引用中。為了演示什么是構造函數的引用,我們創建一個新的函數式接口:

@FunctionalInterface
public interface Generator<T, R> {
  R create(T t);
}

 

創建一個Apple類:

public class Apple {
  public Apple(String color) {
      this.color = color;
  }
  private String color;

  public String getColor() {
      return color;
  }
  public void setColor(String color) {
      this.color = color;
  }
}

 

現在我們可以使用如下方式來創造一個Apple實例:

Generator<String, Apple> g = Apple::new;
Apple apple = g.create("red");

 

這種通過ClassName::new的寫法就是構造函數的引用。在這里Generator的抽象方法接收一個String類型參數,返回值類型為Apple,這和Apple類的構造函數相符合,所以這里編譯可以通過。它等價於下面的寫法:

Generator<String, Apple> g = (color) -> new Apple(color);
Apple apple = g.create("red");

 

Lambda表達式訪問變量

Lambda表達式可以訪問局部final變量,成員變量和靜態變量。

這里主要說下局部final變量。有無final關鍵字不重要,重要的是確保該變量的值不會被改變就行了。比如下面的例子可以編譯通過:

String hello = "hello lambda";
Runnable r = () -> System.out.println(hello);

 

而下面的這個就會編譯出錯,因為變量hello的值被改變了:

Lambda表達式實戰

假如現在有如下需求:現有一個包含了各種顏色不同重量的蘋果的List,編寫一個方法,從中篩選出滿足要求的蘋果。比如篩選出紅色的蘋果、紅色並且重量大於1kg的蘋果、綠色重量小於0.5kg的蘋果或者紅色大於0.5kg的蘋果等等。

不使用Lambda

在沒有接觸Lambda之前,我們一般會這樣做:

定義一個篩選的接口

import cc.mrbird.java8.domain.Apple;

public interface AppleFilter {
  boolean test(Apple apple);
}

然后根據篩選的條件來編寫各個不同的實現類:

篩選出紅色蘋果的實現方法:

import cc.java8.domain.Apple;

public class RedApple implements AppleFilter {
  @Override
  public boolean test(Apple apple) {
      return "red".equalsIgnoreCase(apple.getColor());
  }
}

篩選出紅色並且重量大於1kg的蘋果的實現方法:

import cc.java8.domain.Apple;

public class RedAndMoreThan1kgApple implements AppleFilter {
  @Override
  public boolean test(Apple apple) {
      return "red".equalsIgnoreCase(apple.getColor()) && apple.getWeight() > 1.0;
  }
}

 

篩選出綠色重量小於0.5kg的蘋果或者紅色大於0.5kg的蘋果的實現方法:

import cc.java8.domain.Apple;

public class GreenAndLessThan05OrRedAndMoreThan05Apple implements AppleFilter {
  @Override
  public boolean test(Apple apple) {
      return ("green".equalsIgnoreCase(apple.getColor()) && apple.getWeight() < 0.5)
              || ("red".equalsIgnoreCase(apple.getColor()) && apple.getWeight() > 0.5);
  }
}

 

篩選蘋果的方法:

import cc.java8.domain.Apple;
import java.util.ArrayList;
import java.util.List;

public class AppleFilterMethod {
  public static List<Apple> filterApple(List<Apple> list, AppleFilter filter) {
      List<Apple> filterList = new ArrayList<>();
      for (Apple apple : list) {
          if (filter.test(apple)) {
              filterList.add(apple);
          }
      }
      return filterList;
  }
}

 

開始篩選蘋果:

List<Apple> appleList = new ArrayList<>();
appleList.add(new Apple("red", 0.4));
appleList.add(new Apple("red", 0.6));
appleList.add(new Apple("red", 1.3));
appleList.add(new Apple("green", 0.2));
appleList.add(new Apple("green", 0.35));
appleList.add(new Apple("green", 1.1));

List<Apple> appleFilterList = AppleFilterMethod.filterApple(appleList, new RedApple());
for (Apple apple : appleFilterList) {
  System.out.println(apple.getColor() + " apple,weight:" + apple.getWeight());
}

 

輸出:

red apple,weight:0.4
red apple,weight:0.6
red apple,weight:1.3

 

剩下的略。

可以看到,我們為了滿足各種篩選條件創造了各種篩選接口的實現類,真正起作用的只有篩選方法中return那一行代碼,剩下的都是一些重復的模板代碼。使用Java 8中的Lambda可以很好的消除這些模板代碼。

使用Lambda

AppleFilter接口實際上就是一個函數式接口,所以它的各種實現可以用Lambda表達式來替代,而無需真正的去寫實現方法。

定義篩選接口:

import cc.java8.domain.Apple;

public interface AppleFilter {
  boolean test(Apple apple);
}

 

篩選蘋果的方法:

import cc.mrbird.java8.domain.Apple;
import java.util.ArrayList;
import java.util.List;

public class AppleFilterMethod {
  public static List<Apple> filterApple(List<Apple> list, AppleFilter filter) {
      List<Apple> filterList = new ArrayList<>();
      for (Apple apple : list) {
          if (filter.test(apple)) {
              filterList.add(apple);
          }
      }
      return filterList;
  }
}

 

接下來便可以開始篩選了:

篩選紅色的蘋果:

List<Apple> appleFilterList = AppleFilterMethod.filterApple(appleList,
      (apple) -> "red".equalsIgnoreCase(apple.getColor()));
for (Apple apple : appleFilterList) {
  System.out.println(apple.getColor() + " apple,weight:" + apple.getWeight());
}

 

輸出:

red apple,weight:0.4
red apple,weight:0.6
red apple,weight:1.3

 

篩選出紅色並且重量大於1kg的蘋果:

 List<Apple> appleFilterList = AppleFilterMethod.filterApple(appleList,
              (apple) -> "red".equalsIgnoreCase(apple.getColor()) && apple.getWeight() > 1.0);
for (Apple apple : appleFilterList) {
  System.out.println(apple.getColor() + " apple,weight:" + apple.getWeight());
}

 

輸出:

red apple,weight:1.3

 

篩選出綠色重量小於0.5kg的蘋果或者紅色大於0.5kg的蘋果:

List<Apple> appleFilterList = AppleFilterMethod.filterApple(appleList,
      (apple) -> ("green".equalsIgnoreCase(apple.getColor()) && apple.getWeight() < 0.5) ||
                  ("red".equalsIgnoreCase(apple.getColor()) && apple.getWeight() > 0.5));
for (Apple apple : appleFilterList) {
  System.out.println(apple.getColor() + " apple,weight:" + apple.getWeight());
}

 

輸出:

red apple,weight:0.6
red apple,weight:1.3
green apple,weight:0.2
green apple,weight:0.35

 

使用Lambda表達式消除了大量的樣板代碼,並且可以靈活的構造篩選條件!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM