Java 8的語言變化--理解Lambda表達式和變化的接口類是如何使Java 8成為新的語言
本文是IBM developerWorks中的一篇介紹Java 8關鍵新特性的文章,它主要關注Lambda表達式和改進的接口。(2014.04.19最后更新) Java 8包含了一組重要的新的語言特性,使你能夠更方便地構造程序。Lambda表達為內聯的代碼塊定義了一種新的語法,給予你與匿名內部類相同的靈活性,但又沒有那么多模板代碼。接口的改變使得能夠為已有接口加入新的特性,而不必打破現有代碼的兼容性。了解這些語言變化是怎樣一起工作的,請閱讀本系列另一篇文章"Java 8并發基礎",可以看到如何在Java 8流中使用Lambda。 Java 8的最大改變就是增加了對Lambda表達式的支持。Lambda表達式一種通過引用進行傳遞的代碼塊。它類似于某些其它語言的閉包:代碼實現了一個功能,可以傳入一個或多個參數,還可以返回一個結果值。閉包被定義在一個上下文中,它可以訪問(在Lambda中是只讀訪問)上下文中的值。 如果你不熟悉閉包,也不必擔心。Java 8的Lambda表達式是幾乎每個Java開發者都熟悉的匿名內部類的一個高效版規范。如果你只想在一個位置實現一個接口,或是創建一個基類的子類時,匿名內部類為此提供了一種內聯實現。Lambda表達式也用于相同的方式,但是它使用一種縮略的語法,使得這些實現比一個標準的內部類定義更為簡潔。 在本文中,你將看到如何在不同的場景下使用Lambda表達式,并且你會學到與Java接口定義相關的擴展。在本文章的姊妹篇JVM并發系列的"Java 8并發基礎"一文中,可以看到更多使用Lambda表達式的例子,包括在Java 8流特性中的應用。進入Lambda Lambda表達式就是Java 8所稱的函數接口的實現:一個接口只定義一個抽象方法。只定義一個抽象方法的限制是非常重要的,因為Lambda表達式的語法并不會使用方法名。相反,該表達式會使用動態類型識別(匹配參數和返回類型,很多動態語言都這么做)去保證提供的Lambda能夠與期望的接口方法兼容。 在清單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表達式很快就贏得了Java8程序員們的青睞。(在本例中,同時使用匿名內部類和Lambda表達式去實現Name類中的一個方法,以方便對這兩種方法進行比較。如果在Lambda中對compareTo()方法進行內聯的話,該表達式將會更加簡潔。)標準的函數式接口 為了應用Lambda,新的包java.util.function中定義了廣泛的函數式接口。它們被歸結為如下幾個類別: 函數:使用一個參數,基于參數的值返回結果。 謂語:使用一個參數,基于參數的值返回布爾結果。 雙函數:使用兩個參數,基于參數的值返回結果。 供應器:不使用任何參數,但會返回結果。 消費者:使用一個參數,但不返回任何結果。多數類別都包含多個不同的變體,以便能夠作用于基本數據類型的參數和返回值。許多接口所定義的方法都可被用于組合對象,如清單2所示:清單2. 組合謂語// use predicate composition to remove matching names
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時,這個組合謂語就將返回true(這就相當于早期Java中的邏輯操作符||)。List.removeIf()方法就應用這個組合謂語去刪除列表中的匹配名字。 Java 8定義了許多有用的java.util.function包中接口的組合接口,但這種組合并不都是一樣的。所有的謂語的變體(DoublePredicate,IntPredicate,LongPredicate和Predicate<T>)都定義了相同的組合與修改方法:and(),negate()和or()。但是Function<T>的基本數據類型變體就沒有定義任何組合與修改方法。如果你擁有使用函數式編程語言的經驗,那么你可能就發會發現這些不同之處和奇怪的忽略。改變接口 在Java 8中,接口(如清單1的Comparator)的結構已發生了改變,部分原因是為了讓Lambda更好用。Java 8之前的接口只能定義常量,以及必須被實現的抽象方法。而Java 8中的接口則能夠定義靜態與默認方法。接口中的靜態方法與抽象類中的靜態方法是完全一樣的。默認方法則更像舊式的接口方法,但提供了該方法的一個實現。該方法實現可用于該接口的實現類,除非它被實現類覆蓋掉了。 默認方法的一個重要特性就是它可以被加入到已有接口中,但又不會破壞已使用了這些接口的代碼的兼容性(除非已有代碼恰巧使用了相同名字的方法,并且其目的與默認方法不同)。這是一個非常強大的功能,Java 8的設計者們利用這一特性為許多已有Java類庫加入了對Lambda表達式的支持。清單3就展示了這樣的一個例子,它是清單1中對名字進行排序的第三種實現方式。清單3. 鍵-提取比較器鏈// 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的比較器(從技術上看,鍵-提取Lambda就是java.util.function.Function<T,R>接口的一個實例,它返回的比較器的類型適用于類型T,而提取的鍵的類型R則要實現Comparable接口)。它還展示了如何使用新的Comparator.thenComparing()默認方法去組合使用比較器,清單3就返回了一個新的比較器,它會先按姓排序,再按名排序。 你也許期望能夠對比較器進行內聯,如: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表達式中加入參數的類型:(Name name1) -> name1.lastName。有了這個輔助信息,編譯才能知道下面它該做些什么。第二種方式是告訴編譯器要傳遞給Function接口(在此處,該接口通過Lambda表達式實現)中comparing()方法的泛型變量T和R的類型。 能夠方便地構建比較器以及比較器鏈是Java 8中很有用的特性,但它的代價是增加了復雜度。Java 7的Comparator接口定義了兩個方法(compare()方法,以及遍布于每個對象中的equals()方法)。而在Java 8中,該接口則定義了18個方法(除了原有的2個方法,還新加入了9個靜態方法和7個默認方法)。你將發現,為了能夠使用Lambda而造成的這種接口膨脹會重現于相當一部分Java標準類庫中。像Lambda那樣使用已有方法 如果一個存在的方法已經實現了你的需求,你可以直接使用一個方法引用對它進行傳遞。清單4展示了這種方法。清單4. 對已有方法使用Lambda
// 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的形為"類名:方法名"的方法引用語法,你可以使用任意方法,就像Lambda表達式那樣。其效果就與你定義一個Lambda表達式去調用該方法一樣。對類的靜態方法,特定對象或Lambda輸入類型的實例方法(如在清單4中,getFirstName()和getLastName()方法就是Name類的實例方法),以及類構造器,都可以使用方法引用。 方法引用不僅方便,因為它們比使用Lambda表達式可能更高效,而且為編譯器提供了更好的類型信息(這也就是為什么在上一節的Lambda中使用.thenComparing()構造Comparator會出現問題,而在清單4卻能正常工作)。如果既可以使用對已有方法的方法引用,也可以使用Lambda表達式,請使用前者。捕獲與非捕獲Lambda 你在本文中已見過的Lambda表達式都是非捕獲的,意即,它們都是把傳入的值當作接口方法參數使用的簡單Lambda表達式。Java 8的捕獲Lambda表達式則是使用外圍環境中的值。捕獲Lambda類似于某些JVM語言(如Scala)使用的閉包,但Java 8的實現與之有所不同,因為來自在外圍環境中的值必須聲明為final。也就是說,這些值要么確實為final(就如同以前的Java版本中由匿名內部類所引用的值),要么在外圍環境中不會被修改。這一規范適用于Lambda表達式和匿名內部類。有一些方法可以繞過對值的final限制。例如,在Lambda中僅使用特定變量的當前值,你可以添加一個新的方法,把這些值作為方法參數,再將捕獲的值(以恰當的接口引用這種形式)返回給Lambda。如果期望一個Lambda去修改外圍環境中的值,那么可以用一個可修改的持有器類(Holder)對這些值進行包裝。 相比于捕獲Lambda,可以更高效地處理非捕獲Lambda,那是因為編譯能夠把它生成為類中的靜態方法,而運行時環境可以直接內聯的調用這些方法。捕獲Lambda也許低效一些,但在相同上下文環境中它至少可以表現的和匿名內部類一樣好。幕后的Lambda Lambda表達式看起來像匿名內部類,但它們的實現方法不同。Java的內部類有很多構造器;每個內部類都會有一個字節碼級別的獨立類文件。這就會產生大量的重復代碼(大部分是在常量池實體中),類加載時會造成大量的運行時開銷,哪怕只有少量的代碼也會有如此后果。 Java 8沒有為Lambda生成獨立的類文件,而是使用了在Java 7中引入的invokedynamic字節碼指令。invokedynamic作用于一個啟動方法,當該方法第一次被調用時它會轉而去創建Lambda表達式的實現。然后,該實現會被返回并被直接調用。這樣就避免了獨立類文件帶來的空間開銷,以及加載類的大量運行時開銷。確切地說,Lambda功能的實現被丟給了啟動程序。目前Java 8生成的啟動程序會在運行時為Lambda創建一個新類,但在將來會使用不同的方法去實現。 Java 8使用的優化使得通過invokedynamic指令實現的Lambda在實際中運行正常。多數其它的JVM語言,包括Scala (2.10.x),都會為閉包使用編譯器生成的內部類。在將來,這些語言可能會轉而使用invokedynamic指令,以便利用到Java 8(及其后繼版本)的優化。Lambda的局限 如在本文開始時我所提到的,Lambda表達式總是某些特殊函數式接口的實現。你可以僅把Lambda當作接口引用去傳遞,而對于其它的接口實現,你也可以只是把Lambda當作這些特定接口去使用。清單5展示了這種局限性,在該示例使用了一對相同的(名稱除外)函數式接口。Java 8編譯接受String::lenght來作為這兩個接口的Lambda實現。但是,在一個Lambd表達式被定義為第一個接口的實例之后,它不能夠用于第二個接口的實例。清單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, using a method reference
b = a::valueA;
System.out.println(b.valueB("abc"));
}
任何對Java接口概念有所了解的人都不會對清單5中的程序感到驚訝,因為那就是Java接口一直所做的事情(除了最后一點,那是Java 8新引入的方法引用)。但是使用其它函數式編程語言,例如Scala,的開發者們則會認為接口的這種限制是不自然的。 函數式編程語言是用函數類型,而不是接口,去定義變量。在這些編程語言中會很普遍的使用高級函數:把函數作為參數傳遞給其它的函數,或者把函數當作值去返回。其結果就是你會得到比Lambda更為靈活的編程風格,這包括使用函數去組合其它函數以構建語句塊的能力。因為Java 8沒有定義函數類型,你不能使用這種方法去組合Lambda表達式。你可以組合接口(如清單3所示),但只能是與Java 8中已寫好的那些接口相關的特定接口。僅在新的java.util.function包內,就特殊設定了43個接口去使用Lambda。把它們加入到數以百計的已有接口中,你將看到這種方法在組合接口時總是會有嚴重的限制。 使用接口而不是在向Java中引入函數類型是一個精妙的選擇。這樣就在防止對Java類庫進行重大改動的同時也能夠對已有類庫使用Lambda表達式。它的壞作用就是對Java 8造成了極大的限制,它只能稱為"接口編程"或是類函數式編程,而不是真正的函數式編程。但依靠JVM上其它語言,也包括函數式語言,的優點,這些限制并不可怕。結論 Lambda是Java語言的最主要擴展,伴著它們的兄弟新特性--方法引用,隨著程序被移植到Java 8,Lambda將很快成為所有Java開發者不可或缺的工具。當與Java 8流結合起來時,Lambda就特別有用。查看文章"JVM并發: Java 8并發基礎",可以了解到將Lambda和流結合起來使用是如何簡化并發編程以及提高程序效率的。