聲明:本文翻譯自The Java? Tutorials(官方文檔)

簡述
匿名類有一個問題,如果匿名類的實現(xiàn)非常簡單,比如一個只包含一個方法的接口,那么這樣的匿名類的語法是笨拙和不清晰的。在這些情況下,你通常會嘗試將功能作為參數(shù)傳遞給另一個方法,比如某人點擊按鈕時,它將發(fā)生的具體行為。lambda表達式允許你這樣做,將功能作為方法的參數(shù),或者把代碼當作數(shù)據(jù)。
上一節(jié),Anonymous Classes ,為你展示了如何實現(xiàn)一個匿名的基礎類。雖然它通常比有名稱的類更加簡潔,對于只有一個方法的類,甚至是匿名類,依然顯得有點過于笨拙。lambda表達式可以讓你更加簡潔的表達這種只有單一方法類的實例。
Lambda表達式使用示例
假如你正在開發(fā)一個社交應用。你想開發(fā)一個功能,使管理員可以執(zhí)行任何類型的動作,如發(fā)送消息給某些滿足一定條件的社交應用成員。下面的表格描述了這個應用的細節(jié):
| 字段 | 描述 |
|---|---|
| 名稱 | 對選定的成員執(zhí)行操作 |
| 主要角色 | 管理員 |
| 前置條件 | 管理員已經(jīng)登錄系統(tǒng) |
| 后置條件 | 只對符合條件的成員進行操作 |
| 主要場景 | 1.管理員指定執(zhí)行某個操作的成員的標準 2.管理指定對選定成員執(zhí)行的操作 3.管理員選擇提交按鈕 4.系統(tǒng)查找所有符合條件的成員 5.系統(tǒng)對符合條件的成員執(zhí)行指定的操作 |
| 擴展 | 管理員可以選擇在指定要執(zhí)行的操作之前或在選擇提交按鈕之前,對符合指定條件的成員進行預覽 |
| 發(fā)生頻率 | 一天多次 |
假如社交應用的成員通過下面的Person類來描述:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
假如社交應用的成員實例存儲在List<Person>中。
本節(jié)以一個簡單的方法開始這個例子。它通過局部類、匿名類逐步改進方法,然后用lambda表達式以一種簡明的方式來完成。本節(jié)中的代碼摘錄自例子RosterTest,Person
方式1:創(chuàng)建搜索匹配一個特征的成員的方法
以一種簡單的方式創(chuàng)建幾個方法,每個方法查找匹配某一特征的成員,比如性別或者年齡。下面的方法打印出大于指定年齡的成員:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
注意:List是一個有序的集合。集合是將多個元素組合成一個單元的對象。集合用于存儲、檢索、操作和傳遞聚集數(shù)據(jù)。更多關于集合的信息,查看Collections路徑。
這種方式可能使得你的應用很脆弱,當你對類進行更新時(比如一個新的數(shù)據(jù)類型)可能導致應用無法工作。假設你更新應用,改變Person類的結構,可能是增加一個不同的成員變量,也可能是用一個新的數(shù)據(jù)類型或算法來記錄年齡信息。為了適應這些變化,你必須要重寫大量的API。另外,如果你想打印一個小于指定年齡的成員,這個類也將給你不必要的限制。這該如何解決呢?
方式2:創(chuàng)建更通用的搜索方法
下面的方法比pringPersonsOlderThan更通用。它打印年齡在一定范圍內(nèi)的成員:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
如果你想要打印指定性別的成員,或者是指定性別和年齡范圍的組合,該怎么辦?當你決定修改Person類,添加其他的屬性比如關系狀態(tài)或者地理定位,該怎么辦?雖然這個方法比pringPersonsOlderThan更通用,但嘗試為每個可能的搜索查詢創(chuàng)建一個單獨的方法仍然會導致代碼的脆弱。相反,你可以將按指定條件搜索的代碼分開在不同的類中。
方式3:在局部類中指定搜索條件代碼
下面的方法打印符合搜索條件的成員:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
這個方法通過調(diào)用CheckPerson參數(shù)tester中的tester.test方法,來檢查每一個包含在List參數(shù)roster中的Person實例。如果方法tester.test返回一個true值,那么Person實例中的printPersons方法將會被調(diào)用。
你通過實現(xiàn)CheckPerson接口,來指定查詢條件:
interface CheckPerson {
boolean test(Person p);
}
下面的類實現(xiàn)CheckPerson接口,來為test方法提供一個實現(xiàn)。這個方法篩選出了有資格為美國服役的成員:如果它的Person參數(shù)是男性并且年齡介于18到25之間,它將會返回true值:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
要使用這個例子,你需要創(chuàng)建一個新的實例并且調(diào)用printPersons方法:
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
雖然這個方式比前面的方式要好一點——當改變Person的結構時,你不需要重寫方法——但你仍然有一些額外的多余的代碼:每一個你計劃要在應用中執(zhí)行的查詢,都需要一個新的接口和局部類。因為CheckPersonEligibleForSelectiveService實現(xiàn)了一個接口,您可以使用匿名類代替局部類,繞過需要為每個搜索聲明一個新類。
方式4:在匿名類中指定查詢條件代碼
在下面的printPersons方法調(diào)用中,其中一個參數(shù)是一個匿名類,這個匿名類篩選了有資格為美國服役的成員:他們必須時男性并且年齡介于18到25歲之間:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
這種方法減少了代碼量,因為你不需要為你想要執(zhí)行的每個搜索創(chuàng)建一個新的類。但是,由于CheckPerson接口只包含一個方法,因此匿名類的語法非常笨重。在本例中,可以使用lambda表達式代替匿名類,如下一節(jié)所述。
方式5:用Lambda表達式指定查詢條件代碼
CheckPerson接口是一個函數(shù)式的接口。函數(shù)式接口是任何只包含一個抽象方法的接口。(一個函數(shù)式接口可能包含一個或者更多默認方法或者靜態(tài)方法.)因為一個函數(shù)式接口只包含一個抽象方法,實現(xiàn)它的時候你可以省略方法名。要做到這一點,您可以使用lambda表達式,而不是使用匿名類表達式:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
您可以使用標準的函數(shù)式接口來代替接口CheckPerson,這將進一步減少所需的代碼量。
方式6:使用帶有標準函數(shù)式接口的Lambda表達式
重新考慮CheckPerson接口:
interface CheckPerson {
boolean test(Person p);
}
這是一個非常簡單的接口。它是一個函數(shù)式接口,因為它只包含一個抽象方法。這個方法帶有一個參數(shù)并且返回一個boolean值。這個方法式如此的簡單,所以它可能沒有必要定義在你的應用中。因此JDK定義了幾個標準的函數(shù)式接口,你可以在java.util.function包中找到它們。
例如,你可以使用Predicate<T>接口代替CheckPerson。這個接口包含一個方法boolean test(T t):
interface Predicate<T> {
boolean test(T t);
}
接口Predicate<T>是一個泛型接口的例子。(更多關于泛型的知識,請看Generics (Updated)。)泛型類型(比如泛型接口)使用尖括號(<>)指定一個或者多個參數(shù)。這個接口只有一個類型參數(shù)T。當您使用實際類型參數(shù)聲明或實例化泛型類型時,你就有了一個參數(shù)化類型。例如,一個參數(shù)化類型Predicate<Person>如下所示:
interface Predicate<Person> {
boolean test(Person t);
}
這個參數(shù)化類型包含一個和CheckPerson.boolean test(Person p)有相同返回值和參數(shù)的方法。因此,你可以使用Predicate<T>代替CheckPerson,如下面的示例:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
因此,下面的方法調(diào)用和你在方式3中調(diào)用printPersons一樣:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
在這個方法中,這并不是使用lambda表達式的惟一可能的地方。下面介紹其他使用Lambda表達式的地方。
方式7:在整個應用中使用Lambda表達式
重新考慮printPersonWithPredicate方法中其他可以使用Lambda表達式的位置:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
該方法檢查List參數(shù)roster中的每個Person實例,通過Predicate參數(shù)tester測試Person實例是否滿足條件。如果滿足,就調(diào)用Person實例的printPerson方法。
除了調(diào)用printPerson方法,你可以通過Lambda表達式指定在Person實例上進行的操作。假設你需要一個類似printPerson功能的Lambda表達式,接收一個參數(shù)(Person類型的對象)并返回void。請記住,要使用lambda表達式,你必須實現(xiàn)一個函數(shù)式接口。在這里,你需要一個包含一個抽象方法的函數(shù)式接口,該抽象方法接收一個Person類參數(shù)并返回void。Consumer<T>接口包含了方法void accept<T t>,恰好符合要求。下面的方法將使用Consumer<Person>的accept方法代替p.printPerson方法:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
因此,下面的方法調(diào)用和你在方式3中調(diào)用printPersons效果是一樣的:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
如果你需要對成員信息做更多的操作而非只是打印。假設你需要驗證成員的個人信息或者獲取他們的聯(lián)系方式,該怎么做?在這種情況下,你需要一個函數(shù)式接口,該接口包含了一個能返回某值的抽象方法。Function<T, R>接口包含了R apply(T t)方法。下面的方法獲取了mapper參數(shù)指定的數(shù)據(jù),并對其執(zhí)行由block參數(shù)指定的操作:
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
下面的方法獲取了每個成員郵箱地址,并且打印出來:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方式8:更廣泛的使用泛型
重新考慮方法processPersonsWithFunction。下面是一個泛型版本:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
打印滿足條件的成員的郵箱地址的方法processElements如下:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
該方法調(diào)用執(zhí)行了如下操作:
1.獲取對象集合。在本例中,獲取了Person對象的集合roster。值得注意的是集合roster是屬于List類型的集合同時也是Iterable類型的對象。
2.篩選出滿足Predicate對象tester指定條件的對象。本例中,Predicate對象由一個Lambda表達式提供,表達式指定了篩選成員的條件。
3.通過Function對象將篩選出的對象映射為一個值。本例中,Function對象由一個返回成員郵箱的Lambda表達式提供。
4.在每一個映射后的值上執(zhí)行由Consumer對象block指定的操作。本例中,Consumer對象是一個Lambda表達式,表達式打印由Function對象返回的電子郵箱地址。
你還可以將這每個操作替換為聚集操作(aggregate operation)。
方式9:使用接受Lambda表達式作為參數(shù)的聚合操作
接下來的示例使用了聚集操作完成了方案8中相同的工作:
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
下面的表格列出了processElements方法執(zhí)行的每個操作與對應聚集操作的對照:
| processElements Action | Aggregate Operation |
|---|---|
| Obtain a source of objects | Stream<E> stream() |
| Filter objects that match a Predicate object | Stream<T> filter(Predicate<? super T> predicate) |
| Map objects to another value as specified by a Function object | <R> Stream<R> map(Function<? super T,? extends R> mapper) |
| Perform an action as specified by a Consumer object | void forEach(Consumer<? super T> action) |
filter,map以及forEach操作都是聚集操作。聚集操作從一個流中獲取需要操作的元素,而不是直接從集合中獲?。ㄟ@就是開始需要調(diào)用stream的原因)。一個流是元素的序列,不同于集合,它不是一個存儲元素的數(shù)據(jù)結構。相反,流通過管道從源(例如集合)中運輸數(shù)據(jù)。管道是一系列的流操作,本例中就是filter-map-forEach。除此之外,聚集操作一般使用Lambda表達式作為其參數(shù),從而達到自定義的目的。
更深入的關于聚合操作的討論,請看Aggregate Operations課程。
在GUI程序中應用Lambda表達式
在圖形用戶界面的應用中響應諸如鍵盤事件、鼠標事件或者滾動事件的時候,你一般都需要創(chuàng)建事件處理器,而這通常涉及到實現(xiàn)某個接口。通常情況下,事件處理接口都是包含一個方法的函數(shù)式接口。
在匿名類一節(jié)中討論了javaFX示例HelloWorld.java,你可以使用Lambda表達式替代匿名類:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
btn.setOnAction方法指定了當btn代表的按鈕被觸發(fā)時需要執(zhí)行的操作。這個方法需要一個EventHandler<ActionEvent>類型的對象。EventHandler<ActionEvent>接口只包含了一個方法,void handle(T event)。這是一個函數(shù)接口,因此你可以如下使用Lambda表達式代替:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Lambda表達式的語法
一個lambda表達式由以下幾個部分組成:
- 包圍在小括號內(nèi)的用逗號分隔的形參列表。
CheckPerson.test方法包含了一個參數(shù)p,代表了Person類的一個實例。
注意:lambda表達式的形參類型是可以省略的。除此之外,如果只有一個形參,小括號也可以省略。例如下面的lambda表達式依然是合法的:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
- 箭頭符號->
- 主體,由單獨的表達式或者語句塊組成。本例中使用如下的表達式:
p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
如果使用了單獨的表達式,那么java運行時對表達式求值然后返回該值?;蛘?,你也可以使用return 語句:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
return語句由于不是一個表達式,因而你必須將它放置在一對花括號({})中。然而你不必將返回void的方法調(diào)用放到花括號中。例如,下面是一個合法的Lambda表達式:
email -> System.out.println(email)
鑒于Lambda看起來和方法聲明很相似;你可以將Lambda表達式看成是匿名方法。
接下來的示例,Calculator展示了接收一個以上形參的Lambda用法:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}
operateBinary方法對兩個整數(shù)進行數(shù)學運算。運算的方法由IntegerMath實例提供。該示例使用Lambda定義了兩個運算,addition和subtraction。以下是示例的輸出:
40 + 2 = 42
20 - 10 = 10
訪問閉包環(huán)境的局部變量
和局部類及匿名類一樣,Lambda表達式也可以捕獲變量;他們對于局部變量有相同的訪問權限。然而和局部及匿名類不同的是,Lambda表達式不存在任何隱匿問題(shadowing issues)。Lambda表達式只是詞法意義上的作用域。這意味著它們沒有從超類繼承任何標識符或者是引入了新的作用域。Lambda中的聲明和在外部作用域中聲明是等價的。下面的示例,LambdaScopeTest,展示了這點:
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
// The following statement causes the compiler to generate
// the error "local variables referenced from a lambda expression
// must be final or effectively final" in statement A:
//
// x = 99;
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x); // Statement A
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
示例的輸出如下:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
如果將Lambda表達式myConsumer中的參數(shù)y替換為x,編譯器會提示錯誤:
Consumer<Integer> myConsumer = (x) -> {
// ...
}
編譯器提示錯誤“變量x已經(jīng)在方法methodInFirstLevel(int)定義了”,因為Lambda表達式并沒有引入新的作用域。因此,你可以直接訪字段、方法以及外部作用域中的局部變量。例如Lambda表達式可以直接訪問methodInFirstLevel的參數(shù)x。需要訪問外部類的變量,需使用 this關鍵字。在該示例中,this.x指向的是成員變量FirstLevel.x。
然而如同局部類及匿名類,Lambda表達式只可以訪問final或者effectively final類型的局部變量和外部作用域參數(shù)。假如在methodFirstLevel的開始處加入如下的賦值語句:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
由于這個賦值語句,變量FirstLevel.x不再是effectively final了。此時編譯器就會在Lambda表達式試圖訪問FirstLevel.x變量的地方輸出錯誤信息,“Lambda表達式所訪問的局部變量必須是final或者effectively final”。
System.out.println("x = " + x);
** 譯者注:** 關于
effectively final作一點補充,在 Java SE 7 中,編譯器對內(nèi)部類中引用的外部變量(即捕獲的變量)要求非常嚴格:如果捕獲的變量沒有被聲明為final就會產(chǎn)生一個編譯錯誤。Java SE 8現(xiàn)在放寬了這個限制——對于 Lambda 表達式和內(nèi)部類,允許在其中捕獲那些符合有效只讀(Effectively final)的局部變量。簡單的說,如果一個局部變量在初始化后從未被修改過,那么它就符合有效只讀的要求,換句話說,加上final后也不會導致編譯錯誤的局部變量就是有效只讀變量。
確定Lambda的類型
我們是怎么決定一個lambda表達式的類型的呢?回憶一下前面篩選男性并且年齡在18到25歲之間的成員的Lambda表達式示例:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
這個Lambda表達式用在以下的兩個方法中:
-
public static void printPersons(List<Person> roster, CheckPerson tester)在方式3中 -
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)在方式6中
當java運行調(diào)用printPersons時,它期望一個CheckPerson數(shù)據(jù)類型,因而Lambda表達式就是這個類型了。然而,當Java運行時調(diào)用printPersonsWithPredicate的時候,它期望一個Predicate<Person>類型,因而Lambda表達式就成為了這個類型的實例。這些方法期望的數(shù)據(jù)類型被稱為目標類型(target type)。為了決定一個Lambda的類型,當java編譯器發(fā)現(xiàn)一個Lambda表達式的時候,它會使用當前上下文或者環(huán)境的目標類型。這也就意味著你只能在編譯器能夠確定目標類型的情形下使用Lambda表達式:
- 變量的聲明
- 賦值
-
return語句 - 數(shù)組初始化
- 方法或者構造函數(shù)參數(shù)
- Lambda表達式
body - 條件表達式 ?:
- 類型轉換
目標類型與方法參數(shù)
對于方法參數(shù),java編譯器使用兩個特性來進行決定目標類型:重載決議(overload resolution)和類型推導(argument inference)。如下面兩個函數(shù)接口(java.lang.Runnable 和 java.util.concurrent.Callable<V>):
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
方法Runnable.run沒有返回值,然而Callable<V>.call有。假設你重載了invoke方法:
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
那么如下語句將會調(diào)用那個函數(shù)?
String s = invoke(() -> "done");
invoke(Callable<T>)方法將會被調(diào)用,因為該方法有返回值而invoke(Runnable)沒有。在這種情況下,Lambda表達式()->"done"的類型為Callable<T>。
序列化
如果Lambda表達式的目標參數(shù)和捕獲參數(shù)都是可以可序列化的,那么該Lambda表達式也是可以序列化的。然而,正如內(nèi)部類一樣,對其序列化是極不提倡的。