Lambda 表達式是 Java 8 中添加的功能。引入 Lambda 表達式的主要目的是為了讓 Java 支援函數式程式設計。 Lambda 表達式是一個可以在不屬于任何類的情況下建立的函數,并且可以像對象一樣被傳遞和執行。
Java lambda 表達式用于實作簡單的單方法接口,與 Java Streams API 配合進行函數式程式設計。
在前幾篇關于 List、Set 和 Map 的文章中,我們已經看到了這幾個 Java 容器很多操作都是通過 Stream 完成的,比如過濾出對象 List 中符合條件的子集時,會使用類似下面的 Stream 操作。
List<A> list = aList.filter(a -> a.getId() > 10).collect(Colletors.toList);
其中
filter
方法裡用到的
a -> a.getId() > 10
就是一個 Lambda 表達式,前面幾篇文章我們主要政策講集合架構裡那幾個常用 Java 容器的使用,對用到 Lambda 的地方知識簡單的說了一下,如果你對各種 Stream 操作有疑問,可以先把本篇 Lambda 相關的内容學完,接下來再仔細梳理 Stream 時就會好了解很多了。

Lambda 表達式和函數式接口
上面說了 lambda 表達式便于實作隻擁有單一方法的接口,同樣在 Java 裡匿名類也用于快速實作接口,隻不過 lambda 相較于匿名類更友善些,在書寫的時候連建立類的步驟也免去了,更适合用在函數式程式設計。
舉個例子來說,函數式程式設計經常用在實作事件 Listener 的時候 。 在 Java 中的事件偵聽器通常被定義為具有單個方法的 Java 接口。下面是一個 Listener 接口示例:
public interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
上面這個 Java 接口定義了一個隻要被監聽對象的狀态發生變化,就會調用的 onStateChange 方法(這裡不用管監聽的是什麼,舉例而已)。 在 Java 8 版本以前,監聽事件變更的程式必須實作此接口才能偵聽狀态更改。
比如說,有一個名為 StateOwner 的類,它可以注冊狀态的事件偵聽器。
public class StateOwner {
public void addStateListener(StateChangeListener listener) { ... }
}
我們可以使用匿名類實作 StateChangeListener 接口,然後為 StateOwner 執行個體添加偵聽器。
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
System.out.println("State changed")
}
});
在 Java 8 引入Lambda 表達式後,我們可以用 Lambda 表達式實作 StateChangeListener 接口會更加友善。現在,把上面例子接口的匿名類實作改為 Lambda 實作,程式會變成這樣:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
在這裡,我們使用的 Lambda 表達式是:
(oldState, newState) -> System.out.println("State changed")
這個 lambda 表達式與 StateChangeListener 接口的 onStateChange() 方法的參數清單和傳回值類型相比對。如果一個 lambda 表達式比對單方法接口中方法的參數清單和傳回值(比如本例中的 StateChangeListener 接口的 onStateChange 方法),則 lambda 表達式将轉換為擁有相同方法簽名的接口實作。 這句話聽着有點繞,下面詳細解釋一下 Lambda 表達式和接口比對的詳細規則。
比對Lambda 與接口的規則
上面例子裡使用的 StateChangeListener 接口有一個特點,其隻有一個未實作的抽象方法,在 Java 裡這樣的接口也叫做函數式接口 (Functional Interface)。将 Java lambda 表達式與接口比對需要滿足一下三個規則:
- 接口是否隻有一個抽象(未實作)方法,即是一個函數式接口?
- lambda 表達式的參數是否與抽象方法的參數比對?
- lambda 表達式的傳回類型是否與單個方法的傳回類型比對?
如果能滿足這三個條件,那麼給定的 lambda 表達式就能與接口成功比對類型。
函數式接口
隻有一個抽象方法的接口被稱為函數是式接口,從 Java 8 開始,Java 接口中可以包含預設方法和靜态方法。預設方法和靜态方法都有直接在接口聲明中定義的實作。這意味着,Java lambda 表達式可以實作擁有多個方法的接口——隻要接口中隻有一個未實作的抽象方法就行。
是以在文章一開頭我說lambda 用于實作單方法接口,是為了讓大家更好的了解,真實的情況是隻要接口中隻存在一個抽象方法,那麼這個接口就能用 lambda 實作。
換句話說,即使接口包含預設方法和靜态方法,隻要接口隻包含一個未實作的抽象方法,它就是函數式接口。比如下面這個接口:
import java.io.IOException;
import java.io.OutputStream;
public interface MyInterface {
void printIt(String text);
default public void printUtf8To(String text, OutputStream outputStream){
try {
outputStream.write(text.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e);
}
}
static void printItToSystemOut(String text){
System.out.println(text);
}
}
即使這個接口包含 3 個方法,它也可以通過 lambda 表達式實作,因為接口中隻有一個抽象方法 printIt沒有被實作。
MyInterface myInterface = (String text) -> {
System.out.print(text);
};
Lambda VS 匿名類
盡管 lambda 表達式和匿名類看起來差不多,但還是有一些值得注意的差異。 主要差別在于,匿名類可以有自己的内部狀态--即成員變量,而 lambda 表達式則不能。
public interface MyEventConsumer {
public void consume(Object event);
}
比如上面這個接口,通過匿名類實作
MyEventConsumer consumer = new MyEventConsumer() {
public void consume(Object event){
System.out.println(event.toString() + " consumed");
}
};
MyEventConsumer 接口的匿名類可以有自己的内部狀态。
MyEventConsumer myEventConsumer = new MyEventConsumer() {
private int eventCount = 0;
public void consume(Object event) {
System.out.println(event.toString() + " consumed " + this.eventCount++ + " times.");
}
};
我們給匿名類,加了一個名為 eventCount 的整型成員變量,用來記錄匿名類 consume 方法被執行的次數。Lambda 表達式則不能像匿名類一樣添加成員變量,是以也成 Lambda 表達式是無狀态的。
推斷 Lamdba 的接口類型
使用匿名類實作函數式接口的時候,必須在 new 關鍵字後指明實作的是哪個接口。比如上面使用過的匿名類例子
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
}
});
但是 lambda 表達式,通常可以從上下文中推斷出類型。例如,可以從 addStateListener() 方法聲明中參數的類型 StateChangeListener 推斷出來,Lambda 表達式要實作的是 StateChangeListener 接口。
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
通常 lambda 表達式參數的類型也可以推斷出來。在上面的示例中,編譯器可以從StateChangeListener 接口的抽象方法 onStateChange() 的方法聲明中推斷出參數 oldState 和 newState 的類型。
Lambda 的參數形式
由于 lambda 表達式實際上隻是個方法,是以 lambda 表達式可以像方法一樣接受參數。Lambda 表達式參數根據參數數量以及是否需要添加類型會有下面幾個形式。
如果表達式的方法不帶參數,那麼可以像下面這樣編寫 Lambda 表達式:
() -> System.out.println("Zero parameter lambda");
如果表達式的方法接受一個參數,則可以像下面這樣編寫 Lambda 表達式:
(param) -> System.out.println("One parameter: " + param);
當 Lambda 表達式隻接收單個參數時,參數清單外的小括号也可以省略掉。
param -> System.out.println("One parameter: " + param);
當 Lambda 表達式接收多個參數時,參數清單的括号就沒法省略了。
如果編譯器無法從 Lambda 比對的函數式接口的方法聲明推斷出參數類型(出現這種情況時,編譯器會提示),則有時可能需要為 Lambda 表達式的參數指定類型。
(Car car) -> System.out.println("The car is: " + car.getName());
Lambda 的方法體
lambda 表達式的方法的方法體,在 Lambda 聲明中的 -> 右側指定:
(oldState, newState) -> System.out.println("State changed")
如果 Lambda 表達式的方法體需要由多行組成,則需要把多行代碼寫在用{ }括起來的代碼塊内。
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
Lamdba 表達式的傳回值
可以從 Lambda 表達式傳回值,就像從方法中傳回值一樣。隻需在 Lambda 的方法體中添加一個 return 語句即可:
(param) -> {
System.out.println("param: " + param);
return "return value";
}
如果 Lambda 表達式所做的隻是計算傳回值并傳回它,我們甚至可以省略 return 語句。
(a1, a2) -> { return a1 > a2; }
// 上面的可以簡寫成,不需要return 語句的
(a1, a2) -> { a1 > a2; }
Lambda 表達式本質上是一個對象,跟其他任何我們使用過的對象一樣, 我們可以将 Lambda 表達式指派給變量并進行傳遞和使用。
public interface MyComparator {
public boolean compare(int a1, int a2);
}
---
MyComparator myComparator = (a1, a2) -> a1 > a2;
boolean result = myComparator.compare(2, 5);
上面的這個例子展示 Lambda 表達式的定義,以及如何将 Lambda 表達式指派給給變量,最後通過調用它實作的接口方法來調用 Lambda 表達式。
外部變量在 Lambda 内的可見性
在某些情況下,Lambda 表達式能夠通路在 Lambda 函數體之外聲明的變量。 Lambda 可以通路以下類型的變量:
- 局部變量
- 執行個體變量
- 靜态變量
**Lambda 内通路局部變量,**Lambda 可以通路在 Lambda 方法體之外聲明的局部變量的值
public interface MyFactory {
public String create(char[] chars);
}
String myString = "Test";
MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};
Lambda 通路執行個體變量,Lambda 表達式還可以通路建立了 Lambda 的對象中的執行個體變量。
public class EventConsumerImpl {
private String name = "MyConsumer";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(this.name);
});
}
}
這裡實際上也是 Lambda 與匿名類的差别之一。匿名類因為可以有自己的執行個體變量,這些變量通過 this 引用來引用。但是,Lambda 不能有自己的執行個體變量,是以 this 始終指向外面包裹 Lambda 的對象。
**Lambda 通路靜态變量,**Lambda 表達式也可以通路靜态變量。這也不奇怪,因為靜态變量可以從 Java 應用程式中的任何地方通路,隻要靜态變量是公共的。
public class EventConsumerImpl {
private static String someStaticVar = "Some text";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(someStaticVar);
});
}
}
把方法引用作為 Lambda
如過編寫的 lambda 表達式所做的隻是使用傳遞給 Lambda 的參數調用另一個方法,那麼 Java裡為 Lambda 實作提供了一種更簡短的形式來表達方法調用。比如說,下面是一個函數式數接口:
public interface MyPrinter{
public void print(String s);
}
接下來我們用 Lambda 表達式實作這個 MyPrinter 接口
MyPrinter myPrinter = (s) -> { System.out.println(s); };
因為 Lambda 的參數隻有一個,方法體也隻包含一行,是以可以簡寫成
MyPrinter myPrinter = s -> System.out.println(s);
又因為 Lambda 方法體内所做的隻是将字元串參數轉發給 System.out.println() 方法,是以我們可以将上面的 Lambda 聲明替換為方法引用。
MyPrinter myPrinter = System.out::println;
注意雙冒号 :: 向 Java 的編譯器指明這是一個方法的引用。引用的方法是雙冒号之後的方法。而擁有引用方法的類或對象則位于雙冒号之前。
我們可以引用以下類型的方法:
- 靜态方法
- 參數對象的執行個體方法
- 執行個體方法
- 類的構造方法
引用類的靜态方法
最容易引用的方法是靜态方法,比如有這麼一個函數式接口和類
public interface Finder {
public int find(String s1, String s2);
}
public class MyClass{
public static int doFind(String s1, String s2){
return s1.lastIndexOf(s2);
}
}
如果我們建立 Lambda 去調用 MyClass 的靜态方法 doFind
Finder finder = (s1, s2) -> MyClass.doFind(s1, s2);
是以我們可以使用 Lambda 直接引用 Myclass 的 doFind 方法。
Finder finder = MyClass::doFind;
引用參數的方法
接下來,如果我們在 Lambda 直接轉發調用的方法是來自參數的方法
public interface Finder {
public int find(String s1, String s2);
}
Finder finder = (s1, s2) -> s1.indexOf(s2);
依然可以通過 Lambda 直接引用
Finder finder = String::indexOf;
這個與上面完全形态的 Lambda 在功能上完全一樣,不過要注意簡版 Lambda 是如何引用單個方法的。 Java 編譯器會嘗試将引用的方法與第一個參數的類型比對,使用第二個參數類型作為引用方法的參數。
引用執行個體方法
我們還也可以從 Lambda 定義中引用執行個體方法。首先,設想有如下接口
public interface Deserializer {
public int deserialize(String v1);
}
該接口表示一個能夠将字元串“反序列化”為 int 的元件。現在有一個 StringConvert 類
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
StringConvert 類 的 convertToInt() 方法與 Deserializer 接口的 deserialize() 方法具有相同的簽名。是以,我們可以建立 StringConverter 的執行個體并從 Lambda 表達式中引用其 convertToInt() 方法,如下所示:
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
// 等同于 Deserializer des = (value) -> stringConverter.convertToInt(value)
上面第二行代碼建立的 Lambda 表達式引用了在第一行建立的 StringConverter 執行個體的 convertToInt 方法。
引用構造方法
最後如果 Lambda 的作用是調用一個類的構造方法,那麼可以通過 Lambda 直接引用類的構造方法。在 Lambda 引用類構造方法的形式如下:
ClassName::new
public interface Factory {
public String create(char[] val);
}
Factory factory = String::new;
// 等同于 Factory factory (chars) -> String.new(chars);