譯 原文作者:Jakob Jenkov 原文鏈接:http://tutorials.jenkov.com/java/lambda-expressions.html
@
Java Lambda表達式是Java8中的新特性。Java lambda表達式是Java進入函數式編程的第一步。因此,Java lambda表達式是可以單獨創建的函數,而無需屬於任何類。Java lambda 表達式可以像對象一樣傳遞並按需執行。
Java lambda表達式通常用於實現簡單的事件監聽/回調,或在Java Streams API 函數式編程時使用。
Java Lambdas和函數式接口
函數式編程通常用於實現事件偵聽器。Java中的事件監聽器通常被定義為具有一個抽象方法的Java接口。
這是一個模擬的單個抽象方法接口示例:
public interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
這個Java接口定義了一個抽象方法,只要狀態發生變化(無論觀察到什么),都將調用該方法。
在Java 7中,你必須實現此接口才能監聽狀態的更改。假設你有一個名為StateOwner的類,可以注冊狀態監聽器。示例如下:
public class StateOwner {
public void addStateLister(StateChangeListener stateChangeListener) {
//do some thing
};
}
在Java 7中,你可以使用匿名接口實現添加監聽器,如下所示:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateLister(new StateChangeListener() {
@Override
public void onStateChange(State oldState, State newState) {
System.out.println("State changed");
}
});
在Java 8中你可以使用Lambda表達式來添加監聽器,如下:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateLister(
(oldState, newState) -> System.out.println("State change")
);
這一部分是Lambda表達式:
(oldState, newState) -> System.out.println("State changed")
lambda表達式與addStateListener()方法的參數的參數類型匹配。如果lambda表達式與參數類型(在本例中為StateChangeListener接口)匹配,則將lambda表達式轉換為實現與該參數相同的接口的函數。
Java lambda表達式只能在它們匹配的類型是單個方法接口的地方使用。
在上面的示例中,lambda表達式作為參數,其中參數類型為StateChangeListener接口。該接口只有一個抽象方法。因此,lambda表達式成功匹配該接口。
將Lambda匹配到接口
單個抽象方法接口有時也稱為函數式接口。將Java lambda表達式與函數式接口進行匹配需要以下步驟:
- 接口是否只有一個抽象方法?
- lambda表達式的參數是否與抽象方法的參數匹配?
- lambda表達式的返回類型是否與抽象方法的返回類型匹配?
如果這三個條件都滿足,則該接口可以匹配給定的lambda表達式。
具有默認方法和靜態方法的接口
從Java 8開始,Java接口可以包含默認方法和靜態方法。默認方法和靜態方法都可以在接口中直接實現。這意味着,Java 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表達式實現,因為只有一個抽象方法。
實現如下:
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.");
}
};
請注意,匿名MyEventConsumer接口實現現在具有一個名為eventCount的屬性。
Lambda表達式不能具有此類屬性。因此,lambda表達式是無狀態的。
Lambda類型推斷
在Java 8之前,在進行匿名接口實現時,必須指定要實現的接口。這是本文開頭的匿名接口實現示例:
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// do something with the old and new state.
}
});
使用lambda表達式時,通常可以從相關的代碼中推斷出類型。例如,可以從addStateListener()方法(StateChangeListener接口上的抽象方法)的方法聲明中推斷參數的接口類型。
這稱為類型推斷。編譯器通過在其他地方尋找類型來推斷參數的類型——在這種情況下為方法定義。這是本文開頭的示例,lambda表達式中並未聲明參數的類型:
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
在lambda表達式中,通常可以推斷參數類型。在上面的示例中,編譯器可以從onStateChange()方法聲明中推斷其類型。因此,從onStateChange()方法的方法聲明中就可以推斷出參數 oldState 和 newState 的類型。
Lambda參數
由於Java lambda表達式實際上只是方法,因此lambda表達式可以像方法一樣接受參數。前面顯示的lambda表達式的(oldState,newState)部分指定lambda表達式使用的參數。這些參數必須與函數式接口的抽象方法參數匹配。在當前這個示例,參數必須與StateChangeListener接口的onStateChange()方法的參數匹配:
public void onStateChange(State oldState, State newState);
首先,lambda表達式中的參數數量必須與方法匹配。
其次,如果你在lambda表達式中指定了任何參數類型,則這些類型也必須匹配。我還沒有向你演示如何在lambda表達式參數上設置類型(本文稍后展示),但是在大多數情況下,你不會用到它。
無參數
如果lambda表達式匹配的方法無參數,則可以這樣寫lambda表達式:
() -> System.out.println("Zero parameter lambda");
請注意,括號中沒有內容。那就是表示lambda不帶任何參數。
一個參數
如果Java lambda表達式匹配的方法有一個參數,則可以這樣寫lambda表達式:
(param) -> System.out.println("One parameter: " + param);
請注意,參數在括號內列出。
當lambda表達式是單個參數時,也可以省略括號,如下所示:
param -> System.out.println("One parameter: " + param);
多個參數
如果Java lambda表達式匹配的方法有多個參數,則需要在括號內列出這些參數。代碼如下:
(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);
僅當方法是單個參數時,才可以省略括號。
指定參數類型
如果編譯器無法從lambda匹配的函數式接口抽象方法推斷參數類型,則有時可能需要為lambda表達式指定參數類型。不用擔心,編譯器會在這種情況下會有提醒。這是一個Java lambda指定參數類型示例:
(Car car) -> System.out.println("The car is: " + car.getName());
如你所見,car參數的類型(Car)寫在參數名稱的前面,就像在其他方法中聲明參數或對接口進行匿名實現時一樣。
Java 11中的var參數類型
在Java 11中,你可以使用var關鍵字作為參數類型。
var關鍵字在Java 10中作為局部變量類型推斷引入。從Java 11開始,var也可以用於lambda參數類型。這是在lambda表達式中使用Java var關鍵字作為參數類型的示例:
Function<String, String> toLowerCase = (var input) -> input.toLowerCase();
Lambda表達式主體
lambda表達式的主體以及它表示的函數/方法的主體在lambda聲明中的->的右側指定:
這是一個示例:
(oldState, newState) -> System.out.println("State changed")
如果你的lambda表達式需要包含多行,則可以將lambda函數主體括在{}括號內,Java在其他地方聲明方法時也需要將其括起來。這是一個例子:
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
Lambda表達式返回值
你可以從Java lambda表達式返回值,就像從方法中返回值一樣。你只需向lambda表達式主體添加一個return,如下所示:
(param) -> {
System.out.println("param: " + param);
return "return value";
}
如果你的lambda表達式只需要計算一個返回值並將其返回,則可以用更短的方式指定返回值。例如這個:
(a1, a2) -> { return a1 > a2; }
你可以寫成:
(a1, a2) -> a1 > a2;
然后,編譯器會斷定表達式 a1> a2 是lambda表達式的返回值。
Lambdas作為對象
Java lambda表達式本質上是一個對象。你可以將變量指向lambda表達式並傳遞,就像處理其他任何對象一樣。這是一個例子:
public interface MyComparator {
public boolean compare(int a1, int a2);
}
MyComparator myComparator = (a1, a2) -> return a1 > a2;
boolean result = myComparator.compare(2, 5);
第一個代碼塊顯示了lambda表達式實現的接口。
第二個代碼塊顯示了lambda表達式的定義,lambda表達式如何分配給變量,以及最后如何通過調用其實現的接口方法來調用lambda表達式。
變量捕獲
在某些情況下,Java lambda表達式能夠訪問在lambda表達式主體外部聲明的變量。
Java lambdas可以捕獲以下類型的變量:
- 局部變量
- 實例變量
- 靜態變量
這些變量捕獲的每一個將在以下各節中進行描述。
局部變量捕獲
Java lambda可以捕獲在lambda主體外部聲明的局部變量的值。為了說明這一點,首先看一下這個函數式接口:
public interface MyFactory {
public String create(char[] chars);
}
現在,看一下實現MyFactory接口的lambda表達式:
MyFactory myFactory = (chars) -> {
return new String(chars);
};
現在,此lambda表達式僅引用傳遞給它的參數值(chars)。但是我們可以改變一下。這是引用在lambda函數主體外部聲明的String變量的更新版本:
String myString = "Test";
MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};
如你所見,lambda表達式主體現在引用了在lambda表達式主體外部聲明的局部變量myString。當且僅當被引用的變量是“有效只讀(如果一個局部變量在初始化后從未被修改過,那么它就是有效只讀)”時才有可能,這意味着在賦值之后它不會改變其值。如果myString變量的值稍后更改,則編譯器將抱怨從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.name的引用。這將捕獲封閉的EventConsumerImpl對象的 name 實例變量。甚至可以在捕獲實例變量后更改其值——該值將反映在lambda內部。
this語義實際上是Java lambda與接口的匿名實現不同的地方之一。匿名接口實現可以有自己的實例變量,這些實例變量可以通過this進行引用。但是,lambda不能擁有自己的實例變量,因此它始終指向封閉的對象。
注意:EventConsumer的設計不是很優雅。我只是這樣寫來說明實例變量捕獲。
靜態變量捕獲
Java lambda表達式還可以捕獲靜態變量。
因為只要可以訪問靜態變量(包作用域或public作用域),Java應用程序中的任何地方都可以訪問靜態變量。
這是一個創建lambda表達式的示例類,該lambda表達式從lambda表達式主體內部引用靜態變量:
public class EventConsumerImpl {
private static String someStaticVar = "Some text";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(someStaticVar);
});
}
}
lambda捕獲到靜態變量后,它的值也可以更改。
同樣,上述類設計不太合理。不要對此考慮太多。該類主要用於向你顯示lambda表達式可以訪問靜態變量。
Lambda方法引用
如果你的lambda表達式所做的只是用傳遞給lambda的參數調用另一個方法,則Java lambda實現提供了更簡潔的方式表示該方法調用。
首先,這是一個函數式接口:
public interface MyPrinter{
public void print(String s);
}
以下是創建實現MyPrinter接口的Java lambda表達式的示例:
MyPrinter myPrinter = (s) -> { System.out.println(s); };
由於lambda主體僅由一個語句組成,因此我們實際上可以省略括號{}。另外,由於lambda方法只有一個參數,因此我們可以省略該參數周圍的括號()。更改之后的lambda表達式:
MyPrinter myPrinter = s -> System.out.println(s);
由於所有lambda主體所做的工作都是將字符串參數轉發給System.out.println()方法,因此我們可以將上述lambda聲明替換為方法引用。以下是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);
}
}
最后是引用靜態方法的Java lambda表達式:
Finder finder = MyClass::doFind;
由於Finder.find()和MyClass.doFind()方法的參數匹配,因此可以創建實現Finder.find()並引用MyClass.doFind()方法的lambda表達式。
參數方法引用
也可以將其中一個參數的方法引用到lambda。
函數式接口如下:
public interface Finder {
public int find(String s1, String s2);
}
該接口用於表示能在s1中搜索s2的出現的部分。下面是一個Java lambda表達式的示例,它調用indexOf() 搜索:
Finder finder = String::indexOf;
這等價以下lambda定義:
Finder finder = (s1, s2) -> s1.indexOf(s2);
請注意簡潔方式版本是如何引用單個方法的。Java編譯器嘗試將引用的方法與第一個參數類型相匹配,使用第二個參數類型作為被引用方法的參數。
實例方法引用
第三,還可以從lambda表達式中引用實例方法。
首先,讓我們來看一個函數式接口定義:
public interface Deserializer {
public int deserialize(String v1);
}
此接口表示一個組件,該組件能夠將字符串“反序列化”為int。
現在看看這個StringConverter類:
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
convertToInt()方法與Deserializer deserialize()方法的deserialize()方法具有相同的簽名。因此,我們可以創建StringConverter的實例,並從Java lambda表達式引用其convertToInt()方法,如下所示:
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
第二行創建的lambda表達式引用在第一行創建的StringConverter實例的convertToInt方法。
構造方法引用
最后,可以引用一個類的構造方法。你可以通過在類名后加上:: new來完成此操作,如下所示:
MyClass::new
來看看如何在lambda表達式中引用構造方法。
函數式接口定義如下:
public interface Factory {
public String create(char[] val);
}
此接口的create()方法與String類中某個構造函數的簽名匹配。因此,此構造函數可以被lambda表達式用到。下面是一個示例:
Factory factory = String::new;
等同於如下lambda表達式:
Factory factory = chars -> new String(chars);
水平有限,難免錯漏,歡迎指出,或直接查看原文!