原文位址 en cn
下載下傳 Demo
Java™ 8 包含一些重要的新的語言功能,為您提供了建構程式的更簡單方式。Lambda 表達式 為内聯代碼塊定義一種新文法,其靈活性與匿名内部類一樣,但樣闆檔案要少得多。接口更改使得接口可以添加到現有接口中,同時又不會破壞與現有代碼的相容性。本文将了解這些更改是如何協同工作的。
Java 8 的最大變化在于添加了對 lambda 表達式 的支援。Lambda 表達式是可按引用傳遞的代碼塊。類似于一些其他程式設計語言中的閉包:它們是實作某項功能的代碼,可接受一個或多個輸入參數,而且可傳回一個結果值。閉包是在一個上下文中定義的,可通路(對于 lambda 表達式而言是隻讀通路)來自上下文的值。
如果您不熟悉閉包,不用害怕。Java 8 lambda 表達式其實是匿名内部類的一種特殊化,而幾乎所有 Java 開發人員都熟悉匿名内部類。匿名内部類提供了一個接口的内聯實作,或者一個基類的子類,我們一般隻會在代碼中的一個地方使用它。Lambda 表達式的使用方式一樣,但它有一個簡寫的文法,使得它們比标準内部類定義更簡潔。
在本文中,您将了解如何在各種情形下使用 lambda 表達式,還将了解 Java 語言
interface
定義的相關擴充。
Java 終于有 Lambda 表達式了~
.NET 關于 Lambda 表達式以及函數委托等的實作,從 2008 年開始,經曆了一個技術的演化過程,參看“沒有 Lambda 演算何來匿名函數——匿名函數(匿名方法和Lambda)、委托、LINQ”和“.NET C# 聲明、執行個體化和使用委托以及委托在 C# 中的發展”~
了解 lambdas
Lambda 表達式始終是 Java 8 稱為函數式接口 的一個對象的實作:定義單一抽象方法的
interface
類。對單一抽象方法的限制很重要,因為 lambda 表達式文法不使用方法名。相反,該表達式使用了 duck typing(比對參數和傳回類型,就像在許多動态語言中所做的那樣)來確定所提供的 lambda 與預期的接口方法相相容。
duck typing,鴨子類型,James Whitcomb Riley 提出的一個著名論斷,“當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”我們并不關心對象是什麼類型,到底是不是鴨子,隻關心行為。
在清單 1 的示例中,使用了 lambda 來對
Name
執行個體進行排序。
main()
方法中的第一個代碼塊使用了一個匿名内部類來實作
Comparator<Name>
接口,第二個代碼塊使用了 lambda 表達式。
清單 1. 匿名内部類與 Lambda 表達式的對比
public class Name {
public final String firstName;
public final String lastName;
public Name(String first, String last) {
firstName = first;
lastName = last;
}
// only needed for chained comparator
public String getFirstName() {
return firstName;
}
// only needed for chained comparator
public String getLastName() {
return lastName;
}
// only needed for direct comparator (not for chained comparator)
public int compareTo(Name other) {
int diff = lastName.compareTo(other.lastName);
if (diff == 0) {
diff = firstName.compareTo(other.firstName);
}
return diff;
}
...
}
public class NameSort {
private static final Name[] NAMES = new Name[] {
new Name("Sally", "Smith"),
...
};
private static void printNames(String caption, Name[] names) {
...
}
public static void main(String[] args) {
// sort array using anonymous inner class
Name[] copy = Arrays.copyOf(NAMES, NAMES.length);
Arrays.sort(copy, new Comparator<Name>() {
@Override
public int compare(Name a, Name b) {
return a.compareTo(b);
}
});
printNames("Names sorted with anonymous inner class:", copy);
// sort array using lambda expression
copy = Arrays.copyOf(NAMES, NAMES.length);
Arrays.sort(copy, (a, b) -> a.compareTo(b));
printNames("Names sorted with lambda expression:", copy);
...
}
}
在 清單 1 中,lambda 用于替代一個慣用匿名内部類。這種慣用内部類在實踐中都很常見,是以 lambda 表達式立即赢得了 Java 8 程式員的器重。(在本例中,内部類和 lambda 都使用在
Name
類中實作的一個方法來執行比較工作。如果
compareTo()
方法代碼内聯在 lambda 中,那麼表達式就不怎麼簡潔了。)
标準函數式接口
新的
java.util.function
包定義旨在使用 lambdas 的廣泛函數式接口。這些接口分為幾大類:
- Function:接受一個參數,基于參數值傳回結果
- Predicate:接受一個參數,基于參數值傳回一個布爾值
- BiFunction:接受兩個參數,基于參數值傳回結果
- Supplier:不接受參數,傳回一個結果
- Consumer:接受一個參數,無結果 (
)void
這些類别中大部分都包含幾個用于處理基本原始參數或傳回類型的變量。許多接口定義可用于組合執行個體的方法,如清單 2 中所示。
清單 2. 組合謂詞
// 使用謂詞組合删除比對的名稱
List<Name> list = new ArrayList<>();
for (Name name : NAMES) {
list.add(name);
}
Predicate<Name> pred1 = name -> "Sally".equals(name.firstName);
Predicate<Name> pred2 = name -> "Queue".equals(name.lastName);
list.removeIf(pred1.or(pred2));
printNames("Names filtered by predicate:", list.toArray(new Name[list.size()]));
清單 2 中的代碼定義了一對
Predicate<Name>
,一個與名 Sally 比對,第二個與姓 Queue 比對。
pred1.or(pred2)
方法調用建構所定義的組合謂詞,方法是依次應用兩個謂詞,如果兩個謂詞之一等于
true
(與 Java 中的邏輯
||
運算符一樣),則傳回
true
。
List.removeIf()
方法應用這個組合謂詞從清單中删除比對的名稱。
Java 8 定義了
java.util.function
接口的許多有用的組合,但組合不一緻。謂詞變量(
DoublePredicate
、
IntPredicate
LongPredicate
和
Predicate<T>
)都定義了相同的組合和修改方法:
and()
negate()
or()
。但
Function<T>
的原始變量不定義任何組合或修改方法。如果您有使用函數式程式設計語言的經驗,那麼您可能會發現這些差異和遺漏很古怪。
更改 interfaces
interfaces
interface
類的結構(比如 清單 1 中使用的
Comparator
)在 Java 8 中有了變化,部分原因是為了讓 lambda 表達式更可用。Java 8 之前的接口隻能定義常量和稍後必須實作的抽象方法。Java 8 增加了在接口中同時定義
static
default
方法的能力。一個接口中的靜态方法實際上與一個抽象類中的靜态方法相同。預設方法更像是舊式的接口方法,但有一個附帶的實作,隻有在重寫方法時才會使用該實作。
預設方法的一個重要特性是,可以将它們添加到一個現有的
interface
中,同時不會破壞與使用該接口的其他代碼的相容性(除非您的現有代碼正好出于另一個目的使用相同的方法名)。這是一個強大的特性,Java 8 設計人員使用它來改進對許多預置 Java 庫的 lambda 表達式的支援。清單 3 顯示一個示例,采用第三種方式對添加到 清單 1 代碼中的名稱進行排序。
清單 3. 串連 key-extractor Comparator
// sort array using key-extractor lambdas
copy = Arrays.copyOf(NAMES, NAMES.length);
Comparator<Name> comp = Comparator.comparing(name -> name.lastName);
comp = comp.thenComparing(name -> name.firstName);
Arrays.sort(copy, comp);
printNames("Names sorted with key extractor comparator:", copy);
清單 3 中的代碼首先展示了如何使用新的
Comparator.comparing()
靜态方法來基于您定義的 key-extraction lambda 建立一個 Comparator(從技術上來講,key-extraction lambda 是
java.util.function.Function<T,R>
接口的一個執行個體,其中生成的 Comparator 的類型在配置設定時與
T
相容,而且所提取的鍵類型
R
實作了
Comparable
接口。)另外還展示如何使用新的
Comparator.thenComparing()
預設方法組合 Comparator,在 清單 3 中,該方法傳回了一個新 comparator,它按照姓氏對第一個數組進行排序,按照名字對第二個數組進行排序。
您可能認為您可以将 comparator 構造函數内聯為:
Comparator<Name> comp = Comparator.comparing(name -> name.lastName)
.thenComparing(name -> name.firstName);
遺憾的是,這對于 Java 8 類型推斷不管用。您需要使用以下任意一種形式為編譯器提供有關靜态方法所傳回結果的預期類型的更多資訊:
Comparator<Name> com1 = Comparator.comparing((Name name1) -> name1.lastName)
.thenComparing(name2 -> name2.firstName);
Comparator<Name> com2 = Comparator.<Name,String>comparing(name1 -> name1.lastName)
.thenComparing(name2 -> name2.firstName);
第一種形式将 lambda 參數的類型添加到 lambda 表達式:
(Name name1) -> name1.lastName
。有了這一協助,編譯器就可以了解其餘要做的工作是什麼。第二種形式将傳遞給
comparing()
方法的函數式接口(在本例中由 lambda 實作)的類型
T
R
告訴編譯器。
輕松構造和串連 comparator 的能力是 Java 8 的一個有用功能,但其代價是增加了複雜性。Java 7
Comparator
接口定義了兩個方法(
compare()
和保證要為每個對象定義的無處不在的
equals()
)。Java 8 版本定義了 18 個方法(原始的 2 個方法,加上 9 個新靜态方法和 7 個新的預設方法)。您會發現,為使用 lambdas 而産生的這一大規模接口膨脹模式會在 Java 标準庫的相當一部分中重複出現。
使用 lambdas 這樣的現有方法
如果有一個現有的方法已經滿足了您的需要,那麼您可以使用方法引用 來直接傳遞該方法。清單 4 展示了該方法。
清單 4. 使用 lambdas 這樣的現有方法
...
// sort array using existing methods as lambdas
copy = Arrays.copyOf(NAMES, NAMES.length);
comp = Comparator.comparing(Name::getLastName).thenComparing(Name::getFirstName);
Arrays.sort(copy, comp);
printNames("Names sorted with existing methods as lambdas:", copy);
清單 4 與 清單 3 中實作的功能一樣,不同的是使用了現有的方法。您可以使用 Java 8
ClassName::methodName
方法引用文法,像使用 lambda 表達式一樣使用任意方法。這與定義調用該方法的 lambda 具有完全相同的效果。您可以對靜态方法、lambda 的特定對象或輸入類型的執行個體方法(如 清單 4 所示,其中
getFirstName()
getLastName()
方法是所比較的
Name
的執行個體方法)以及構造函數使用方法引用。
方法引用不僅使用友善,比起使用 lambda 表達式它們可能更有效,而且對于編譯器(這就是為什麼在清單 4 最後一部分對比 lambdas 出現問題而使用方法引用工作正常的原因)也提供了更好的類型資訊。如果您在使用一個已經存在的方法引用和使用一個 lambda 之間做出選擇,您應該總是更傾向于使用方法引用。
捕獲的和非捕獲的 lambdas
本文中您看到的 lambda 示例都是非捕獲的,也就是說,它們是簡單的表達式,僅使用作為接口方法參數的等效值傳遞進來的值。Java 8 中捕獲的 lambdas 使用了所包含的上下文中的值。捕獲的 lambdas 類似于其他一些 JVM 語言(包括 Scala)中使用的閉包,但不同之處在于,在 Java 8 中,所包含的上下文中的任何值必須是 effectively final。即該值必須是真正的
final
(因為引用自匿名内部類的值必須在早期 Java 版本中)或者 在上下文中從未被修改過。這一标準同時适用于 lambda 表達式和匿名内部類使用的值。
您可以使用一些解決方法來應對 effectively final 限制。例如,如果要在一個 lambda 表達式中僅使用某些變量的目前值,那麼您可以添加一個新方法,接受這些值作為參數,并為 lambda 表達式(以适當接口引用的形式)傳回捕獲的值。如果想要一個 lambda 表達式來修改封閉的上下文中的值,那麼可以将該值包裝到一個可變容器中。
與捕獲的 lambdas 相比,非捕獲的 lambdas 可以得到更高效的處理,因為編譯器可以将它們生成為包含類中的靜态方法,而且運作時可以直接内聯調用。捕獲的 lambdas 可能效率稍差一點,但在相同的上下文中它的性能應至少與匿名内部類一樣。
Lambdas 幕後揭秘
Lambda 表達式看起來非常像匿名内部類,但實作方式不同。Java 内部類是龐大的構造函數;一直到位元組碼級别,每個内部類都有一個獨立的類檔案。很多資料是重複的(主要采用常量池項的形式),類加載增加了相當大的運作時開銷,這一切都隻是為了支援少量增加的代碼。
Java 8 沒有為 lambdas 使用獨立的類檔案,而是依賴 Java 7 中添加的
invokedynamic
位元組碼指令。
invokedynamic
以一個 bootstrap 方法為目标,該方法在首次被調用時建立 lambda 表達式實作。随後,傳回的實作被直接調用。這樣就避免了獨立類檔案的空間開銷以及加載類的大量運作時開銷。lambda 函數究竟是如何 實作的就交由 bootstrap 來決定。Java 8 目前生成的 bootstrap 代碼在運作時為 lambda 建構了一個新類,但未來的實作可自由使用不同的方法。
Java 8 結合了一些優化措施,使得通過
invokedynamic
進行的 lambdas 實作在實踐中行之有效。其他大部分 JVM 語言,包括 Scala (2.10.x),為閉包使用編譯器生成的内部類。這些語言的未來版本可能轉向
invokedynamic
方法,以便利用 Java 8(和更高版本)的優化。
Lambda 的限制
正如我在文章開頭所提到的,lambda 表達式幾乎是一些特殊函數接口的實作。您隻可以通過 lambdas 作為接口引用和其他接口的實作,您隻可使用一個 lambda 作為将要建立的具體接口。清單 5 通過一對相同(除了名稱)的函數式接口展示了這一限制。Java 8 編譯器接受
String::length
方法作為兩個接口的 lambda 實作。但在将 lambda 定義為第一個接口的執行個體之後,就不能将其用作第二個接口的執行個體。
清單 5. Lambda 的限制
private interface A {
public int valueA(String s);
}
private interface B {
public int valueB(String s);
}
public static void main(String[] args) {
A a = String::length;
B b = String::length;
// compiler error!
// b = a;
// ClassCastException at runtime!
// b = (B)a;
// works, but ugly (wraps in a new lambda)
b = (x) -> a.valueA(x);
System.out.println(b.valueB("abc"));
}
Scala 等函數式程式設計語言使用函數類型(而不是接口)來定義變量。在這種語言中使用高階函數 是很常見的事情:高階函數是将函數作為參數傳遞或将函數作為值傳回的函數。其程式設計風格要比 lambdas 靈活得多,包括能夠将函數作為建構塊來組建其他函數。由于 Java 8 沒有定義函數類型,是以您不能以這種方式建立 lambdas。您可以建立接口(如 清單 3 所示),但編寫的代碼僅用于處理所涉及到的特定接口。僅僅在新的
java.util.function
程式包中,就專門建立了 43 個用于 lambdas 的接口。将這些接口添加到上百個現有接口中,您可以看到建構接口的方式總是受到極大的限制。
在進行使用接口(而不是添加函數類型到 Java)的選擇時,一定要深思熟慮。這樣做會排除對 Java 庫進行重大變動的需要,同時支援對現有的庫使用 lambda 表達式。這樣做的弊端在于,它将 Java 8 限定為所謂的 “接口程式設計” 或類似函數式的程式設計,而非真正的函數式程式設計。但随着 JVM 上開始支援其他多種語言,包括函數式語言,這一限制就沒那麼嚴重了。
結束語
Lambdas 是 Java 語言的一個重大擴充,而且随着所有 Java 人員将其應用程式遷移到 Java 8,Lambda 表達式很快将成為他們不可缺少的一個工具。在與 Java 8 streams 結合使用時,Lambdas 特别有用。參閱 “JVM 并發性:Java 8 并發性基礎”,了解 lambdas 與 Java 8 streams 如何共同簡化并發程式設計和提高應用程式性能。
參考資料
- Lambda 表達式:Java 教程中的這一主題解釋了在各種上下文中使用 lambda 表達式的細節。
- Lambda: A Peek Under the Hood:檢視由 Java 語言架構師和 IBM developerWorks 作家 Brian Goetz 制作的這一 JavaOne 2013 示範文稿,了解 Java 8 lambda 表達式的設計和實作背後的邏輯。
- 使用 Lambda 表達式在 Java 中程式設計:在由 Venkat Subramaniam 于 JavaOne 2013 上提供的這一實時編碼示範中檢視 lambda 使用示例。