作者:Lucida
原文鏈接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features
本文謝絕轉(zhuǎn)載,如需轉(zhuǎn)載需征得作者本人同意,謝謝。
本文是深入理解Java 8 Lambda系列的第一篇,主要介紹Java 8新增的語言特性(比如lambda和方法引用),語言概念(比如目標(biāo)類型和變量捕獲)以及設(shè)計(jì)思路。
本文是對(duì)Brian Goetz的State of Lambda一文的翻譯,那么問題來了:
為什么要寫(翻譯)這個(gè)系列?關(guān)鍵在于這些文章和書都沒有解決我對(duì)Java lambda的困惑,比如:
為了加深理解,我決定翻譯這一系列文章
如果你不知道什么是函數(shù)式編程,或者不了解map,filter,reduce這些常用的高階函數(shù),那么你不適合閱讀本文,請(qǐng)先學(xué)習(xí)函數(shù)式編程基礎(chǔ)(比如這本書)。
關(guān)于The high-level goal of PRoject Lambda is to enable programming patterns that require modeling code as data to be convenient and idiomatic in Java.
本文介紹了Java SE 8中新引入的lambda語言特性以及這些特性背后的設(shè)計(jì)思想。這些特性包括:
Java是一門面向?qū)ο缶幊陶Z言。面向?qū)ο缶幊陶Z言和函數(shù)式編程語言中的基本元素(Basic Values)都可以動(dòng)態(tài)封裝程序行為:面向?qū)ο缶幊陶Z言使用帶有方法的對(duì)象封裝行為,函數(shù)式編程語言使用函數(shù)封裝行為。但這個(gè)相同點(diǎn)并不明顯,因?yàn)镴ava的對(duì)象往往比較“重量級(jí)”:實(shí)例化一個(gè)類型往往會(huì)涉及不同的類,并需要初始化類里的字段和方法。
不過有些Java對(duì)象只是對(duì)單個(gè)函數(shù)的封裝。例如下面這個(gè)典型用例:Java API中定義了一個(gè)接口(一般被稱為回調(diào)接口),用戶通過提供這個(gè)接口的實(shí)例來傳入指定行為,例如:
public interface ActionListener { void actionPerformed(ActionEvent e);}這里并不需要專門定義一個(gè)類來實(shí)現(xiàn)ActionListener接口,因?yàn)樗粫?huì)在調(diào)用處被使用一次。用戶一般會(huì)使用匿名類型把行為內(nèi)聯(lián)(inline):
button.addActionListener(new ActionListener) { public void actionPerformed(ActionEvent e) { ui.dazzle(e.getModifiers()); }}很多庫都依賴于上面的模式。對(duì)于并行API更是如此,因?yàn)槲覀冃枰汛龍?zhí)行的代碼提供給并行API,并行編程是一個(gè)非常值得研究的領(lǐng)域,因?yàn)樵谶@里摩爾定律得到了重生:盡管我們沒有更快的CPU核心(core),但是我們有更多的CPU核心。而串行API就只能使用有限的計(jì)算能力。
隨著回調(diào)模式和函數(shù)式編程風(fēng)格的日益流行,我們需要在Java中提供一種盡可能輕量級(jí)的將代碼封裝為數(shù)據(jù)(Model code as data)的方法。匿名內(nèi)部類并不是一個(gè)好的選擇,因?yàn)椋?/p>
this和變量名容易使人產(chǎn)生誤解final的局部變量上面的多數(shù)問題均在Java SE 8中得以解決:
不過,Java SE 8的目標(biāo)并非解決所有上述問題。因此捕獲可變變量(問題4)和非局部控制流(問題5)并不在Java SE 8的范疇之內(nèi)。(盡管我們可能會(huì)在未來提供對(duì)這些特性的支持)
2. 函數(shù)式接口(Functional interfaces)盡管匿名內(nèi)部類有著種種限制和問題,但是它有一個(gè)良好的特性,它和Java類型系統(tǒng)結(jié)合的十分緊密:每一個(gè)函數(shù)對(duì)象都對(duì)應(yīng)一個(gè)接口類型。之所以說這個(gè)特性是良好的,是因?yàn)椋?/p>
上面提到的ActionListener接口只有一個(gè)方法,大多數(shù)回調(diào)接口都擁有這個(gè)特征:比如Runnable接口和Comparator接口。我們把這些只擁有一個(gè)方法的接口稱為函數(shù)式接口。(之前它們被稱為SAM類型,即單抽象方法類型(Single Abstract Method))
我們并不需要額外的工作來聲明一個(gè)接口是函數(shù)式接口:編譯器會(huì)根據(jù)接口的結(jié)構(gòu)自行判斷(判斷過程并非簡(jiǎn)單的對(duì)接口方法計(jì)數(shù):一個(gè)接口可能冗余的定義了一個(gè)Object已經(jīng)提供的方法,比如toString(),或者定義了靜態(tài)方法或默認(rèn)方法,這些都不屬于函數(shù)式接口方法的范疇)。不過API作者們可以通過@FunctionalInterface注解來顯式指定一個(gè)接口是函數(shù)式接口(以避免無意聲明了一個(gè)符合函數(shù)式標(biāo)準(zhǔn)的接口),加上這個(gè)注解之后,編譯器就會(huì)驗(yàn)證該接口是否滿足函數(shù)式接口的要求。
實(shí)現(xiàn)函數(shù)式類型的另一種方式是引入一個(gè)全新的結(jié)構(gòu)化函數(shù)類型,我們也稱其為“箭頭”類型。例如,一個(gè)接收String和Object并返回int的函數(shù)類型可以被表示為(String, Object) -> int。我們仔細(xì)考慮了這個(gè)方式,但出于下面的原因,最終將其否定:
m(T->U)和m(X->Y)進(jìn)行重載(Overload)所以我們選擇了“使用已知類型”這條路——因?yàn)楝F(xiàn)有的類庫大量使用了函數(shù)式接口,通過沿用這種模式,我們使得現(xiàn)有類庫能夠直接使用lambda表達(dá)式。例如下面是Java SE 7中已經(jīng)存在的函數(shù)式接口:
除此之外,Java SE 8中增加了一個(gè)新的包:java.util.function,它里面包含了常用的函數(shù)式接口,例如:
Predicate<T>——接收T對(duì)象并返回booleanConsumer<T>——接收T對(duì)象,不返回值Function<T, R>——接收T對(duì)象,返回R對(duì)象Supplier<T>——提供T對(duì)象(例如工廠),不接收值UnaryOperator<T>——接收T對(duì)象,返回T對(duì)象BinaryOperator<T>——接收兩個(gè)T對(duì)象,返回T對(duì)象除了上面的這些基本的函數(shù)式接口,我們還提供了一些針對(duì)原始類型(Primitive type)的特化(Specialization)函數(shù)式接口,例如IntSupplier和LongBinaryOperator。(我們只為int、long和double提供了特化函數(shù)式接口,如果需要使用其它原始類型則需要進(jìn)行類型轉(zhuǎn)換)同樣的我們也提供了一些針對(duì)多個(gè)參數(shù)的函數(shù)式接口,例如BiFunction<T, U, R>,它接收T對(duì)象和U對(duì)象,返回R對(duì)象。
匿名類型最大的問題就在于其冗余的語法。有人戲稱匿名類型導(dǎo)致了“高度問題”(height problem):比如前面ActionListener的例子里的五行代碼中僅有一行在做實(shí)際工作。
lambda表達(dá)式是匿名方法,它提供了輕量級(jí)的語法,從而解決了匿名內(nèi)部類帶來的“高度問題”。
下面是一些lambda表達(dá)式:
(int x, int y) -> x + y() -> 42(String s) -> { System.out.println(s); }第一個(gè)lambda表達(dá)式接收x和y這兩個(gè)整形參數(shù)并返回它們的和;第二個(gè)lambda表達(dá)式不接收參數(shù),返回整數(shù)'42';第三個(gè)lambda表達(dá)式接收一個(gè)字符串并把它打印到控制臺(tái),不返回值。
lambda表達(dá)式的語法由參數(shù)列表、箭頭符號(hào)->和函數(shù)體組成。函數(shù)體既可以是一個(gè)表達(dá)式,也可以是一個(gè)語句塊:
語句塊:語句塊中的語句會(huì)被依次執(zhí)行,就像方法中的語句一樣——
return語句會(huì)把控制權(quán)交給匿名方法的調(diào)用者break和continue只能在循環(huán)中使用表達(dá)式函數(shù)體適合小型lambda表達(dá)式,它消除了return關(guān)鍵字,使得語法更加簡(jiǎn)潔。
lambda表達(dá)式也會(huì)經(jīng)常出現(xiàn)在嵌套環(huán)境中,比如說作為方法的參數(shù)。為了使lambda表達(dá)式在這些場(chǎng)景下盡可能簡(jiǎn)潔,我們?nèi)コ瞬槐匾姆指舴2贿^在某些情況下我們也可以把它分為多行,然后用括號(hào)包起來,就像其它普通表達(dá)式一樣。
下面是一些出現(xiàn)在語句中的lambda表達(dá)式:
FileFilter java = (File f) -> f.getName().endsWith("*.java");String user = doPrivileged(() -> System.getProperty("user.name"));new Thread(() -> { connectToService(); sendNotification();}).start();4. 目標(biāo)類型(Target typing)需要注意的是,函數(shù)式接口的名稱并不是lambda表達(dá)式的一部分。那么問題來了,對(duì)于給定的lambda表達(dá)式,它的類型是什么?答案是:它的類型是由其上下文推導(dǎo)而來。例如,下面代碼中的lambda表達(dá)式類型是ActionListener:
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());這就意味著同樣的lambda表達(dá)式在不同上下文里可以擁有不同的類型:
Callable<String> c = () -> "done";PrivilegedAction<String> a = () -> "done";第一個(gè)lambda表達(dá)式() -> "done"是Callable的實(shí)例,而第二個(gè)lambda表達(dá)式則是PrivilegedAction的實(shí)例。
編譯器負(fù)責(zé)推導(dǎo)lambda表達(dá)式的類型。它利用lambda表達(dá)式所在上下文所期待的類型進(jìn)行推導(dǎo),這個(gè)被期待的類型被稱為目標(biāo)類型。lambda表達(dá)式只能出現(xiàn)在目標(biāo)類型為函數(shù)式接口的上下文中。
當(dāng)然,lambda表達(dá)式對(duì)目標(biāo)類型也是有要求的。編譯器會(huì)檢查lambda表達(dá)式的類型和目標(biāo)類型的方法簽名(method signature)是否一致。當(dāng)且僅當(dāng)下面所有條件均滿足時(shí),lambda表達(dá)式才可以被賦給目標(biāo)類型T:
T是一個(gè)函數(shù)式接口T的方法參數(shù)在數(shù)量和類型上一一對(duì)應(yīng)T的方法返回值相兼容(Compatible)T的方法throws類型相兼容由于目標(biāo)類型(函數(shù)式接口)已經(jīng)“知道”lambda表達(dá)式的形式參數(shù)(Formal parameter)類型,所以我們沒有必要把已知類型再重復(fù)一遍。也就是說,lambda表達(dá)式的參數(shù)類型可以從目標(biāo)類型中得出:
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);在上面的例子里,編譯器可以推導(dǎo)出s1和s2的類型是String。此外,當(dāng)lambda的參數(shù)只有一個(gè)而且它的類型可以被推導(dǎo)得知時(shí),該參數(shù)列表外面的括號(hào)可以被省略:
FileFilter java = f -> f.getName().endsWith(".java");button.addActionListener(e -> ui.dazzle(e.getModifiers()));這些改進(jìn)進(jìn)一步展示了我們的設(shè)計(jì)目標(biāo):“不要把高度問題轉(zhuǎn)化成寬度問題。”我們希望語法元素能夠盡可能的少,以便代碼的讀者能夠直達(dá)lambda表達(dá)式的核心部分。
lambda表達(dá)式并不是第一個(gè)擁有上下文相關(guān)類型的Java表達(dá)式:泛型方法調(diào)用和“菱形”構(gòu)造器調(diào)用也通過目標(biāo)類型來進(jìn)行類型推導(dǎo):
List<String> ls = Collections.emptyList();List<Integer> li = Collections.emptyList();Map<String, Integer> m1 = new HashMap<>();Map<Integer, String> m2 = new HashMap<>();5. 目標(biāo)類型的上下文(Contexts for target typing)之前我們提到lambda表達(dá)式智能出現(xiàn)在擁有目標(biāo)類型的上下文中。下面給出了這些帶有目標(biāo)類型的上下文:
? :)在前三個(gè)上下文(變量聲明、賦值和返回語句)里,目標(biāo)類型即是被賦值或被返回的類型:
Comparator<String> c;c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);public Runnable toDoLater() { return () -> { System.out.println("later"); }}數(shù)組初始化器和賦值類似,只是這里的“變量”變成了數(shù)組元素,而類型是從數(shù)組類型中推導(dǎo)得知:
filterFiles(new FileFilter[] { f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("q") });方法參數(shù)的類型推導(dǎo)要相對(duì)復(fù)雜些:目標(biāo)類型的確認(rèn)會(huì)涉及到其它兩個(gè)語言特性:重載解析(Overload resolution)和參數(shù)類型推導(dǎo)(Type argument inference)。
重載解析會(huì)為一個(gè)給定的方法調(diào)用(method invocation)尋找最合適的方法聲明(method declaration)。由于不同的聲明具有不同的簽名,當(dāng)lambda表達(dá)式作為方法參數(shù)時(shí),重載解析就會(huì)影響到lambda表達(dá)式的目標(biāo)類型。編譯器會(huì)通過它所得之的信息來做出決定。如果lambda表達(dá)式具有顯式類型(參數(shù)類型被顯式指定),編譯器就可以直接 使用lambda表達(dá)式的返回類型;如果lambda表達(dá)式具有隱式類型(參數(shù)類型被推導(dǎo)而知),重載解析則會(huì)忽略lambda表達(dá)式函數(shù)體而只依賴lambda表達(dá)式參數(shù)的數(shù)量。
如果在解析方法聲明時(shí)存在二義性(ambiguous),我們就需要利用轉(zhuǎn)型(cast)或顯式lambda表達(dá)式來提供更多的類型信息。如果lambda表達(dá)式的返回類型依賴于其參數(shù)的類型,那么lambda表達(dá)式函數(shù)體有可能可以給編譯器提供額外的信息,以便其推導(dǎo)參數(shù)類型。
List<Person> ps = ...Stream<String> names = ps.stream().map(p -> p.getName());在上面的代碼中,ps的類型是List<Person>,所以ps.stream()的返回類型是Stream<Person>。map()方法接收一個(gè)類型為Function<T, R>的函數(shù)式接口,這里T的類型即是Stream元素的類型,也就是Person,而R的類型未知。由于在重載解析之后lambda表達(dá)式的目標(biāo)類型仍然未知,我們就需要推導(dǎo)R的類型:通過對(duì)lambda表達(dá)式函數(shù)體進(jìn)行類型檢查,我們發(fā)現(xiàn)函數(shù)體返回String,因此R的類型是String,因而map()返回Stream<String>。絕大多數(shù)情況下編譯器都能解析出正確的類型,但如果碰到無法解析的情況,我們則需要:
p提供顯式類型)以提供額外的類型信息Function<Person, String>R提供一個(gè)實(shí)際類型。(.<String>map(p -> p.getName()))lambda表達(dá)式本身也可以為它自己的函數(shù)體提供目標(biāo)類型,也就是說lambda表達(dá)式可以通過外部目標(biāo)類型推導(dǎo)出其內(nèi)部的返回類型,這意味著我們可以方便的編寫一個(gè)返回函數(shù)的函數(shù):
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };類似的,條件表達(dá)式可以把目標(biāo)類型“分發(fā)”給其子表達(dá)式:
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);最后,轉(zhuǎn)型表達(dá)式(Cast expression)可以顯式提供lambda表達(dá)式的類型,這個(gè)特性在無法確認(rèn)目標(biāo)類型時(shí)非常有用:
// Object o = () -> { System.out.println("hi"); }; 這段代碼是非法的Object o = (Runnable) () -> { System.out.println("hi"); };除此之外,當(dāng)重載的方法都擁有函數(shù)式接口時(shí),轉(zhuǎn)型可以幫助解決重載解析時(shí)出現(xiàn)的二義性。
目標(biāo)類型這個(gè)概念不僅僅適用于lambda表達(dá)式,泛型方法調(diào)用和“菱形”構(gòu)造方法調(diào)用也可以從目標(biāo)類型中受益,下面的代碼在Java SE 7是非法的,但在Java SE 8中是合法的:
List<String> ls = Collections.checkedList(new ArrayList<>(), String.class);Set<Integer> si = flag ? Collections.singleton(23) : Collections.emptySet();6. 詞法作用域(Lexical scoping)在內(nèi)部類中使用變量名(以及this)非常容易出錯(cuò)。內(nèi)部類中通過繼承得到的成員(包括來自Object的方法)可能會(huì)把外部類的成員掩蓋(shadow),此外未限定(unqualified)的this引用會(huì)指向內(nèi)部類自己而非外部類。
相對(duì)于內(nèi)部類,lambda表達(dá)式的語義就十分簡(jiǎn)單:它不會(huì)從超類(supertype)中繼承任何變量名,也不會(huì)引入一個(gè)新的作用域。lambda表達(dá)式基于詞法作用域,也就是說lambda表達(dá)式函數(shù)體里面的變量和它外部環(huán)境的變量具有相同的語義(也包括lambda表達(dá)式的形式參數(shù))。此外,'this'關(guān)鍵字及其引用在lambda表達(dá)式內(nèi)部和外部也擁有相同的語義。
為了進(jìn)一步說明詞法作用域的優(yōu)點(diǎn),請(qǐng)參考下面的代碼,它會(huì)把"Hello, world!"打印兩遍:
public class Hello { Runnable r1 = () -> { System.out.println(this); } Runnable r2 = () -> { System.out.println(toString()); } public String toString() { return "Hello, world"; } public static void main(String... args) { new Hello().r1.run(); new Hello().r2.run(); }}與之相類似的內(nèi)部類實(shí)現(xiàn)則會(huì)打印出類似Hello$1@5b89a773和Hello$2@537a7706之類的字符串,這往往會(huì)使開發(fā)者大吃一驚。
基于詞法作用域的理念,lambda表達(dá)式不可以掩蓋任何其所在上下文中的局部變量,它的行為和那些擁有參數(shù)的控制流結(jié)構(gòu)(例如for循環(huán)和catch從句)一致。
個(gè)人補(bǔ)充:這個(gè)說法很拗口,所以我在這里加一個(gè)例子以演示詞法作用域:
int i = 0;int sum = 0;for (int i = 1; i < 10; i += 1) { //這里會(huì)出現(xiàn)編譯錯(cuò)誤,因?yàn)閕已經(jīng)在for循環(huán)外部聲明過了 sum += i;}7. 變量捕獲(Variable capture)在Java SE 7中,編譯器對(duì)內(nèi)部類中引用的外部變量(即捕獲的變量)要求非常嚴(yán)格:如果捕獲的變量沒有被聲明為final就會(huì)產(chǎn)生一個(gè)編譯錯(cuò)誤。我們現(xiàn)在放寬了這個(gè)限制——對(duì)于lambda表達(dá)式和內(nèi)部類,我們?cè)试S在其中捕獲那些符合有效只讀(Effectively final)的局部變量。
簡(jiǎn)單的說,如果一個(gè)局部變量在初始化后從未被修改過,那么它就符合有效只讀的要求,換句話說,加上final后也不會(huì)導(dǎo)致編譯錯(cuò)誤的局部變量就是有效只讀變量。
Callable<String> helloCallable(String name) { String hello = "Hello"; return () -> (hello + ", " + name);}對(duì)this的引用,以及通過this對(duì)未限定字段的引用和未限定方法的調(diào)用在本質(zhì)上都屬于使用final局部變量。包含此類引用的lambda表達(dá)式相當(dāng)于捕獲了this實(shí)例。在其它情況下,lambda對(duì)象不會(huì)保留任何對(duì)this的引用。
這個(gè)特性對(duì)內(nèi)存管理是一件好事:內(nèi)部類實(shí)例會(huì)一直保留一個(gè)對(duì)其外部類實(shí)例的強(qiáng)引用,而那些沒有捕獲外部類成員的lambda表達(dá)式則不會(huì)保留對(duì)外部類實(shí)例的引用。要知道內(nèi)部類的這個(gè)特性往往會(huì)造成內(nèi)存泄露。
盡管我們放寬了對(duì)捕獲變量的語法限制,但試圖修改捕獲變量的行為仍然會(huì)被禁止,比如下面這個(gè)例子就是非法的:
int sum = 0;list.forEach(e -> { sum += e.size(); });為什么要禁止這種行為呢?因?yàn)檫@樣的lambda表達(dá)式很容易引起race condition。除非我們能夠強(qiáng)制(最好是在編譯時(shí))這樣的函數(shù)不能離開其當(dāng)前線程,但如果這么做了可能會(huì)導(dǎo)致更多的問題。簡(jiǎn)而言之,lambda表達(dá)式對(duì)值封閉,對(duì)變量開放。
個(gè)人補(bǔ)充:lambda表達(dá)式對(duì)值封閉,對(duì)變量開放的原文是:lambda expressions close over values, not variables,我在這里增加一個(gè)例子以說明這個(gè)特性:
int sum = 0;list.forEach(e -> { sum += e.size(); }); // Illegal, close over valuesList<Integer> aList = new List<>();list.forEach(e -> { aList.add(e); }); // Legal, open over variableslambda表達(dá)式不支持修改捕獲變量的另一個(gè)原因是我們可以使用更好的方式來實(shí)現(xiàn)同樣的效果:使用規(guī)約(reduction)。java.util.stream包提供了各種通用的和專用的規(guī)約操作(例如sum、min和max),就上面的例子而言,我們可以使用規(guī)約操作(在串行和并行下都是安全的)來代替forEach:
int sum = list.stream() .mapToInt(e -> e.size()) .sum();sum()等價(jià)于下面的規(guī)約操作:
int sum = list.stream() .mapToInt(e -> e.size()) .reduce(0 , (x, y) -> x + y);規(guī)約需要一個(gè)初始值(以防輸入為空)和一個(gè)操作符(在這里是加號(hào)),然后用下面的表達(dá)式計(jì)算結(jié)果:
0 + list[0] + list[1] + list[2] + ...規(guī)約也可以完成其它操作,比如求最小值、最大值和乘積等等。如果操作符具有可結(jié)合性(associative),那么規(guī)約操作就可以容易的被并行化。所以,與其支持一個(gè)本質(zhì)上是并行而且容易導(dǎo)致race condition的操作,我們選擇在庫中提供一個(gè)更加并行友好且不容易出錯(cuò)的方式來進(jìn)行累積(accumulation)。
8. 方法引用(Method references)lambda表達(dá)式允許我們定義一個(gè)匿名方法,并允許我們以函數(shù)式接口的方式使用它。我們也希望能夠在已有的方法上實(shí)現(xiàn)同樣的特性。
方法引用和lambda表達(dá)式擁有相同的特性(例如,它們都需要一個(gè)目標(biāo)類型,并需要被轉(zhuǎn)化為函數(shù)式接口的實(shí)例),不過我們并不需要為方法引用提供方法體,我們可以直接通過方法名稱引用已有方法。
以下面的代碼為例,假設(shè)我們要按照name或age為Person數(shù)組進(jìn)行排序:
class Person { private final String name; private final int age; public int getAge() { return age; } public String getName() {return name; } ...}Person[] people = ...Comparator<Person> byName = Comparator.comparing(p -> p.getName());Arrays.sort(people, byName);在這里我們可以用方法引用代替lambda表達(dá)式:
Comparator<Person> byName = Comparator.comparing(Person::getName);這里的Person::getName可以被看作為lambda表達(dá)式的簡(jiǎn)寫形式。盡管方法引用不一定(比如在這個(gè)例子里)會(huì)把語法變的更緊湊,但它擁有更明確的語義——如果我們想要調(diào)用的方法擁有一個(gè)名字,我們就可以通過它的名字直接調(diào)用它。
因?yàn)楹瘮?shù)式接口的方法參數(shù)對(duì)應(yīng)于隱式方法調(diào)用時(shí)的參數(shù),所以被引用方法簽名可以通過放寬類型,裝箱以及組織到參數(shù)數(shù)組中的方式對(duì)其參數(shù)進(jìn)行操作,就像在調(diào)用實(shí)際方法一樣:
Consumer<Integer> b1 = System::exit; // void exit(int status)Consumer<String[]> b2 = Arrays:sort; // void sort(Object[] a)Consumer<String> b3 = MyProgram::main; // void main(String... args)Runnable r = Myprogram::mapToInt // void main(String... args)9. 方法引用的種類(Kinds of method references)方法引用有很多種,它們的語法如下:
ClassName::methodNameinstanceReference::methodNamesuper::methodNameClassName::methodNameClass::newTypeName[]::new對(duì)于靜態(tài)方法引用,我們需要在類名和方法名之間加入::分隔符,例如Integer::sum。
對(duì)于具體對(duì)象上的實(shí)例方法引用,我們則需要在對(duì)象名和方法名之間加入分隔符:
Set<String> knownNames = ...Predicate<String> isKnown = knownNames::contains;這里的隱式lambda表達(dá)式(也就是實(shí)例方法引用)會(huì)從knownNames中捕獲String對(duì)象,而它的方法體則會(huì)通過Set.contains使用該String對(duì)象。
有了實(shí)例方法引用,在不同函數(shù)式接口之間進(jìn)行類型轉(zhuǎn)換就變的很方便:
Callable<Path> c = ...Privileged<Path> a = c::call;引用任意對(duì)象的實(shí)例方法則需要在實(shí)例方法名稱和其所屬類型名稱間加上分隔符:
Function<String, String> upperfier = String::toUpperCase;這里的隱式lambda表達(dá)式(即String::toUpperCase實(shí)例方法引用)有一個(gè)String參數(shù),這個(gè)參數(shù)會(huì)被toUpperCase方法使用。
如果類型的實(shí)例方法是泛型的,那么我們就需要在::分隔符前提供類型參數(shù),或者(多數(shù)情況下)利用目標(biāo)類型推導(dǎo)出其類型。
需要注意的是,靜態(tài)方法引用和類型上的實(shí)例方法引用擁有一樣的語法。編譯器會(huì)根據(jù)實(shí)際情況做出決定。
一般我們不需要指定方法引用中的參數(shù)類型,因?yàn)榫幾g器往往可以推導(dǎo)出結(jié)果,但如果需要我們也可以顯式在::分隔符之前提供參數(shù)類型信息。
和靜態(tài)方法引用類似,構(gòu)造方法也可以通過new關(guān)鍵字被直接引用:
SocketImplFactory factory = MySocketImpl::new;如果類型擁有多個(gè)構(gòu)造方法,那么我們就會(huì)通過目標(biāo)類型的方法參數(shù)來選擇最佳匹配,這里的選擇過程和調(diào)用構(gòu)造方法時(shí)的選擇過程是一樣的。
如果待實(shí)例化的類型是泛型的,那么我們可以在類型名稱之后提供類型參數(shù),否則編譯器則會(huì)依照"菱形"構(gòu)造方法調(diào)用時(shí)的方式進(jìn)行推導(dǎo)。
數(shù)組的構(gòu)造方法引用的語法則比較特殊,為了便于理解,你可以假想存在一個(gè)接收int參數(shù)的數(shù)組構(gòu)造方法。參考下面的代碼:
IntFunction<int[]> arrayMaker = int[]::new;int[] array = arrayMaker.apply(10) // 創(chuàng)建數(shù)組 int[10]10. 默認(rèn)方法和靜態(tài)接口方法(Default and static interface methods)lambda表達(dá)式和方法引用大大提升了Java的表達(dá)能力(expressiveness),不過為了使把代碼即數(shù)據(jù)(code-as-data)變的更加容易,我們需要把這些特性融入到已有的庫之中,以便開發(fā)者使用。
Java SE 7時(shí)代為一個(gè)已有的類庫增加功能是非常困難的。具體的說,接口在發(fā)布之后就已經(jīng)被定型,除非我們能夠一次性更新所有該接口的實(shí)現(xiàn),否則向接口添加方法就會(huì)破壞現(xiàn)有的接口實(shí)現(xiàn)。默認(rèn)方法(之前被稱為虛擬擴(kuò)展方法或守護(hù)方法)的目標(biāo)即是解決這個(gè)問題,使得接口在發(fā)布之后仍能被逐步演化。
這里給出一個(gè)例子,我們需要在標(biāo)準(zhǔn)集合API中增加針對(duì)lambda的方法。例如removeAll方法應(yīng)該被泛化為接收一個(gè)函數(shù)式接口Predicate,但這個(gè)新的方法應(yīng)該被放在哪里呢?我們無法直接在Collection接口上新增方法——不然就會(huì)破壞現(xiàn)有的Collection實(shí)現(xiàn)。我們倒是可以在Collections工具類中增加對(duì)應(yīng)的靜態(tài)方法,但這樣就會(huì)把這個(gè)方法置于“二等公民”的境地。
默認(rèn)方法利用面向?qū)ο蟮姆绞较蚪涌谠黾有碌男袨椤K且环N新的方法:接口方法可以是抽象的或是默認(rèn)的。默認(rèn)方法擁有其默認(rèn)實(shí)現(xiàn),實(shí)現(xiàn)接口的類型通過繼承得到該默認(rèn)實(shí)現(xiàn)(如果類型沒有覆蓋該默認(rèn)實(shí)現(xiàn))。此外,默認(rèn)方法不是抽象方法,所以我們可以放心的向函數(shù)式接口里增加默認(rèn)方法,而不用擔(dān)心函數(shù)式接口的單抽象方法限制。
下面的例子展示了如何向Iterator接口增加默認(rèn)方法skip:
interface Iterator<E> { boolean hasNext(); E next(); void remove(); default void skip(int i) { for ( ; i > 0 && hasNext(); i -= 1) next(); }}根據(jù)上面的Iterator定義,所有實(shí)現(xiàn)Iterator的類型都會(huì)自動(dòng)繼承skip方法。在使用者的眼里,skip不過是接口新增的一個(gè)虛擬方法。在沒有覆蓋skip方法的Iterator子類實(shí)例上調(diào)用skip會(huì)執(zhí)行skip的默認(rèn)實(shí)現(xiàn):調(diào)用hasNext和next若干次。子類可以通過覆蓋skip來提供更好的實(shí)現(xiàn)——比如直接移動(dòng)游標(biāo)(cursor),或是提供為操作提供原子性(Atomicity)等。
當(dāng)接口繼承其它接口時(shí),我們既可以為它所繼承而來的抽象方法提供一個(gè)默認(rèn)實(shí)現(xiàn),也可以為它繼承而來的默認(rèn)方法提供一個(gè)新的實(shí)現(xiàn),還可以把它繼承而來的默認(rèn)方法重新抽象化。
除了默認(rèn)方法,Java SE 8還在允許在接口中定義靜態(tài)方法。這使得我們可以從接口直接調(diào)用和它相關(guān)的輔助方法(Helper method),而不是從其它的類中調(diào)用(之前這樣的類往往以對(duì)應(yīng)接口的復(fù)數(shù)命名,例如Collections)。比如,我們一般需要使用靜態(tài)輔助方法生成實(shí)現(xiàn)Comparator的比較器,在Java SE 8中我們可以直接把該靜態(tài)方法定義在Comparator接口中:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<T, U> keyExtractor) { return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));}11. 繼承默認(rèn)方法(Inheritance of default methods)和其它方法一樣,默認(rèn)方法也可以被繼承,大多數(shù)情況下這種繼承行為和我們所期待的一致。不過,當(dāng)類型或者接口的超類擁有多個(gè)具有相同簽名的方法時(shí),我們就需要一套規(guī)則來解決這個(gè)沖突:
為了演示第二條規(guī)則,我們假設(shè)Collection和List接口均提供了removeAll的默認(rèn)實(shí)現(xiàn),然后Queue繼承并覆蓋了Collection中的默認(rèn)方法。在下面的implement從句中,List中的方法聲明會(huì)優(yōu)先于Queue中的方法聲明:
class LinkedList<E> implements List<E>, Queue<E> { ... }當(dāng)兩個(gè)獨(dú)立的默認(rèn)方法相沖突或是默認(rèn)方法和抽象方法相沖突時(shí)會(huì)產(chǎn)生編譯錯(cuò)誤。這時(shí)程序員需要顯式覆蓋超類方法。一般來說我們會(huì)定義一個(gè)默認(rèn)方法,然后在其中顯式選擇超類方法:
interface Robot implements Artist, Gun { default void draw() { Artist.super.draw(); }}super前面的類型必須是有定義或繼承默認(rèn)方法的類型。這種方法調(diào)用并不只限于消除命名沖突——我們也可以在其它場(chǎng)景中使用它。
最后,接口在inherits和extends從句中的聲明順序和它們被實(shí)現(xiàn)的順序無關(guān)。
我們?cè)谠O(shè)計(jì)lambda時(shí)的一個(gè)重要目標(biāo)就是新增的語言特性和庫特性能夠無縫結(jié)合(designed to work together)。接下來,我們通過一個(gè)實(shí)際例子(按照姓對(duì)名字列表進(jìn)行排序)來演示這一點(diǎn):
比如說下面的代碼:
List<Person> people = ...Collections.sort(people, new Comparator<Person>() { public int compare(Person x, Person y) { return x.getLastName().compareTo(y.getLastName()); }})冗余代碼實(shí)在太多了!
有了lambda表達(dá)式,我們可以去掉冗余的匿名類:
Collections.sort(people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));盡管代碼簡(jiǎn)潔了很多,但它的抽象程度依然很差:開發(fā)者仍然需要進(jìn)行實(shí)際的比較操作(而且如果比較的值是原始類型那么情況會(huì)更糟),所以我們要借助Comparator里的comparing方法實(shí)現(xiàn)比較操作:
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));在類型推導(dǎo)和靜態(tài)導(dǎo)入的幫助下,我們可以進(jìn)一步簡(jiǎn)化上面的代碼:
Collections.sort(people, comparing(p -> p.getLastName()));我們注意到這里的lambda表達(dá)式實(shí)際上是getLastName的代理(forwarder),于是我們可以用方法引用代替它:
Collections.sort(people, comparing(Person::getLastName));最后,使用Collections.sort這樣的輔助方法并不是一個(gè)好主意:它不但使代碼變的冗余,也無法為實(shí)現(xiàn)List接口的數(shù)據(jù)結(jié)構(gòu)提供特定(specialized)的高效實(shí)現(xiàn),而且由于Collections.sort方法不屬于List接口,用戶在閱讀List接口的文檔時(shí)不會(huì)察覺在另外的Collections類中還有一個(gè)針對(duì)List接口的排序(sort())方法。
默認(rèn)方法可以有效的解決這個(gè)問題,我們?yōu)?code>List增加默認(rèn)方法sort(),然后就可以這樣調(diào)用:
people.sort(comparing(Person::getLastName));;此外,如果我們?yōu)?code>Comparator接口增加一個(gè)默認(rèn)方法reversed()(產(chǎn)生一個(gè)逆序比較器),我們就可以非常容易的在前面代碼的基礎(chǔ)上實(shí)現(xiàn)降序排序。
people.sort(comparing(Person::getLastName).reversed());;13. 小結(jié)(Summary)Java SE 8提供的新語言特性并不算多——lambda表達(dá)式,方法引用,默認(rèn)方法和靜態(tài)接口方法,以及范圍更廣的類型推導(dǎo)。但是把它們結(jié)合在一起之后,開發(fā)者可以編寫出更加清晰簡(jiǎn)潔的代碼,類庫編寫者可以編寫更加強(qiáng)大易用的并行類庫。
未完待續(xù)——
下篇:深入理解Java 8 Lambda(類庫篇——Streams API,Collector和并行)
作者:Lucida
原文鏈接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features
本文謝絕轉(zhuǎn)載,如需轉(zhuǎn)載需征得作者本人同意,謝謝。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注