国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計(jì) > 正文

深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標(biāo)類型和默認(rèn)方法)

2019-11-14 21:37:19
字體:
供稿:網(wǎng)友
深入理解java 8 Lambda(語言篇——lambda,方法引用,目標(biāo)類型和默認(rèn)方法)

作者:Lucida

  • 微博:@peng_gong
  • 豆瓣:@figure9

原文鏈接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features

本文謝絕轉(zhuǎn)載,如需轉(zhuǎn)載需征得作者本人同意,謝謝。

  1. 深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標(biāo)類型和默認(rèn)方法)
  2. 深入理解Java 8 Lambda(類庫篇——Streams API,Collector和并行)
  3. 深入理解Java 8 Lambda(原理篇——Java編譯器如何處理lambda)
關(guān)于

本文是深入理解Java 8 Lambda系列的第一篇,主要介紹Java 8新增的語言特性(比如lambda和方法引用),語言概念(比如目標(biāo)類型和變量捕獲)以及設(shè)計(jì)思路。

本文是對(duì)Brian Goetz的State of Lambda一文的翻譯,那么問題來了:

為什么要寫(翻譯)這個(gè)系列?
  1. 工作之后,我開始大量使用Java
  2. 公司將會(huì)在不久的未來使用Java 8
  3. 作為資質(zhì)平庸的開發(fā)者,我需要打一點(diǎn)提前量,以免到時(shí)拙計(jì)
  4. 為了學(xué)習(xí)Java 8(主要是其中的lambda及相關(guān)庫),我先后閱讀了Oracle的官方文檔,Cay Horstmann(Core Java的作者)的Java 8 for the Really Impatient和Richard Warburton的Java 8 Lambdas
  5. 但我感到并沒有多大收獲,Oracle的官方文檔涉及了lambda表達(dá)式的每一個(gè)概念,但都是點(diǎn)到輒止;后兩本書(尤其是Java 8 Lambdas)花了大量篇幅介紹Java lambda及其類庫,但實(shí)質(zhì)內(nèi)容不多,讀完了還是沒有對(duì)Java lambda產(chǎn)生一個(gè)清晰的認(rèn)識(shí)
  6. 關(guān)鍵在于這些文章和書都沒有解決我對(duì)Java lambda的困惑,比如:

    • Java 8中的lambda為什么要設(shè)計(jì)成這樣?(為什么要一個(gè)lambda對(duì)應(yīng)一個(gè)接口?而不是Structural Typing?)
    • lambda和匿名類型的關(guān)系是什么?lambda是匿名對(duì)象的語法糖嗎?
    • Java 8是如何對(duì)lambda進(jìn)行類型推導(dǎo)的?它的類型推導(dǎo)做到了什么程度?
    • Java 8為什么要引入默認(rèn)方法?
    • Java編譯器如何處理lambda?
    • 等等……
  7. 之后我在Google搜索這些問題,然后就找到Brian Goetz的三篇關(guān)于Java lambda的文章(State of Lambda,State of Lambda libraries version和Translation of lambda),讀完之后上面的問題都得到了解決
  8. 為了加深理解,我決定翻譯這一系列文章

警告(Caveats)

如果你不知道什么是函數(shù)式編程,或者不了解mapfilterreduce這些常用的高階函數(shù),那么你不適合閱讀本文,請(qǐng)先學(xué)習(xí)函數(shù)式編程基礎(chǔ)(比如這本書)。


State of Lambda by Brian Goetz

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.

關(guān)于

本文介紹了Java SE 8中新引入的lambda語言特性以及這些特性背后的設(shè)計(jì)思想。這些特性包括:

  • lambda表達(dá)式(又被成為“閉包”或“匿名方法”)
  • 方法引用和構(gòu)造方法引用
  • 擴(kuò)展的目標(biāo)類型和類型推導(dǎo)
  • 接口中的默認(rèn)方法和靜態(tài)方法
1. 背景

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>

  1. 語法過于冗余
  2. 匿名類中的this和變量名容易使人產(chǎn)生誤解
  3. 類型載入和實(shí)例創(chuàng)建語義不夠靈活
  4. 無法捕獲非final的局部變量
  5. 無法對(duì)控制流進(jìn)行抽象

上面的多數(shù)問題均在Java SE 8中得以解決:

  • 通過提供更簡(jiǎn)潔的語法和局部作用域規(guī)則,Java SE 8徹底解決了問題1和問題2
  • 通過提供更加靈活而且便于優(yōu)化的表達(dá)式語義,Java SE 8繞開了問題3
  • 通過允許編譯器推斷變量的“常量性”(finality),Java SE 8減輕了問題4帶來的困擾

不過,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>

  • 接口是Java類型系統(tǒng)的一部分
  • 接口天然就擁有其運(yùn)行時(shí)表示(Runtime representation)
  • 接口可以通過Javadoc注釋來表達(dá)一些非正式的協(xié)定(contract),例如,通過注釋說明該操作應(yīng)可交換(commutative)

上面提到的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è)接收StringObject并返回int的函數(shù)類型可以被表示為(String, Object) -> int。我們仔細(xì)考慮了這個(gè)方式,但出于下面的原因,最終將其否定:

  • 它會(huì)為Java類型系統(tǒng)引入額外的復(fù)雜度,并帶來結(jié)構(gòu)類型(Structural Type)和指名類型(Nominal Type)的混用。(Java幾乎全部使用指名類型)
  • 它會(huì)導(dǎo)致類庫風(fēng)格的分歧——一些類庫會(huì)繼續(xù)使用回調(diào)接口,而另一些類庫會(huì)使用結(jié)構(gòu)化函數(shù)類型
  • 它的語法會(huì)變得十分笨拙,尤其在包含受檢異常(checked exception)之后
  • 每個(gè)函數(shù)類型很難擁有其運(yùn)行時(shí)表示,這意味著開發(fā)者會(huì)受到類型擦除(erasure)的困擾和局限。比如說,我們無法對(duì)方法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.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.beans.PropertyChangeListener

除此之外,Java SE 8中增加了一個(gè)新的包:java.util.function,它里面包含了常用的函數(shù)式接口,例如:

  • Predicate<T>——接收T對(duì)象并返回boolean
  • Consumer<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ù)式接口,例如IntSupplierLongBinaryOperator。(我們只為intlongdouble提供了特化函數(shù)式接口,如果需要使用其它原始類型則需要進(jìn)行類型轉(zhuǎn)換)同樣的我們也提供了一些針對(duì)多個(gè)參數(shù)的函數(shù)式接口,例如BiFunction<T, U, R>,它接收T對(duì)象和U對(duì)象,返回R對(duì)象。

3. lambda表達(dá)式(lambda expressions)

匿名類型最大的問題就在于其冗余的語法。有人戲稱匿名類型導(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á)式接收xy這兩個(gè)整形參數(shù)并返回它們的和;第二個(gè)lambda表達(dá)式不接收參數(shù),返回整數(shù)'42';第三個(gè)lambda表達(dá)式接收一個(gè)字符串并把它打印到控制臺(tái),不返回值。

lambda表達(dá)式的語法由參數(shù)列表、箭頭符號(hào)->和函數(shù)體組成。函數(shù)體既可以是一個(gè)表達(dá)式,也可以是一個(gè)語句塊:

  • 表達(dá)式:表達(dá)式會(huì)被執(zhí)行然后返回執(zhí)行結(jié)果。
  • 語句塊:語句塊中的語句會(huì)被依次執(zhí)行,就像方法中的語句一樣——

    • return語句會(huì)把控制權(quán)交給匿名方法的調(diào)用者
    • breakcontinue只能在循環(huán)中使用
    • 如果函數(shù)體有返回值,那么函數(shù)體內(nèi)部的每一條路徑都必須返回值

表達(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ù)式接口
  • lambda表達(dá)式的參數(shù)和T的方法參數(shù)在數(shù)量和類型上一一對(duì)應(yīng)
  • lambda表達(dá)式的返回值和T的方法返回值相兼容(Compatible)
  • lambda表達(dá)式內(nèi)所拋出的異常和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)出s1s2的類型是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)類型的上下文:

  • 變量聲明
  • 賦值
  • 返回語句
  • 數(shù)組初始化器
  • 方法和構(gòu)造方法的參數(shù)
  • lambda表達(dá)式函數(shù)體
  • 條件表達(dá)式(? :
  • 轉(zhuǎn)型(Cast)表達(dá)式

在前三個(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ù)情況下編譯器都能解析出正確的類型,但如果碰到無法解析的情況,我們則需要:

  • 使用顯式lambda表達(dá)式(為參數(shù)p提供顯式類型)以提供額外的類型信息
  • 把lambda表達(dá)式轉(zhuǎn)型為Function<Person, String>
  • 為泛型參數(shù)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@5b89a773Hello$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 variables

lambda表達(dá)式不支持修改捕獲變量的另一個(gè)原因是我們可以使用更好的方式來實(shí)現(xiàn)同樣的效果:使用規(guī)約(reduction)。java.util.stream包提供了各種通用的和專用的規(guī)約操作(例如summinmax),就上面的例子而言,我們可以使用規(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è)我們要按照nameagePerson數(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)

方法引用有很多種,它們的語法如下:

  • 靜態(tài)方法引用:ClassName::methodName
  • 實(shí)例上的實(shí)例方法引用:instanceReference::methodName
  • 超類上的實(shí)例方法引用:super::methodName
  • 類型上的實(shí)例方法引用:ClassName::methodName
  • 構(gòu)造方法引用:Class::new
  • 數(shù)組構(gòu)造方法引用:TypeName[]::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)用hasNextnext若干次。子類可以通過覆蓋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è)沖突:

  • 類的方法(class method)聲明優(yōu)先于接口默認(rèn)方法。無論該方法是具體的還是抽象的。
  • 被其它類型所覆蓋的方法會(huì)被忽略。這條規(guī)則適用于超類型共享一個(gè)公共祖先的情況。

為了演示第二條規(guī)則,我們假設(shè)CollectionList接口均提供了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)景中使用它。

最后,接口在inheritsextends從句中的聲明順序和它們被實(shí)現(xiàn)的順序無關(guān)。

12. 融會(huì)貫通(Putting it together)

我們?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

  • 微博:@peng_gong
  • 豆瓣:@figure9

原文鏈接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features

本文謝絕轉(zhuǎn)載,如需轉(zhuǎn)載需征得作者本人同意,謝謝。


發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 隆子县| 璧山县| 龙口市| 浑源县| 葫芦岛市| 南澳县| 盐边县| 佛山市| 吉隆县| 巴林左旗| 泸定县| 资中县| 固阳县| 英德市| 彭泽县| 噶尔县| 沧源| 三亚市| 海阳市| 英吉沙县| 舟山市| 峨山| 北海市| 尉氏县| 阿合奇县| 沈阳市| 承德市| 南陵县| 西安市| 双柏县| 隆德县| 海宁市| 中卫市| 益阳市| 铜陵市| 高雄县| 舞阳县| 南丹县| 曲阳县| 湖州市| 繁峙县|