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必須滿足以下兩個條件:
-
實現的對象是函數式接口的抽象方法;
-
函數式接口的抽象方法的函數描述符和Lambda表達式的函數描述符一致。
函數式接口
函數式接口的定義開頭已經說了,這里就不再贅述。在Java 8之前,常見的函數式接口有java.util.Comparator,java.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表達式消除了大量的樣板代碼,並且可以靈活的構造篩選條件!