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