深入理解Java 8 Lambda 語言篇 類庫篇
http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/
http://zh.lucida.me/blog/java-8-lambdas-inside-out-library-features/
深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標型別和預設方法)
關於
- 深入理解 Java 8 Lambda(語言篇——lambda,方法引用,目標型別和預設方法)
- 深入理解 Java 8 Lambda(類庫篇——Streams API,Collector 和並行)
- 深入理解 Java 8 Lambda(原理篇——Java 編譯器如何處理 lambda)
本文是深入理解 Java 8 Lambda 系列的第一篇,主要介紹 Java 8 新增的語言特性(比如 lambda 和方法引用),語言概念(比如目標型別和變數捕獲)以及設計思路。
本文是對 Brian Goetz 的 State of Lambda 一文的翻譯,那麼問題來了:
為什麼要翻譯這個系列?
- 工作之後,我開始大量使用 Java
- 公司將會在不久的未來使用 Java 8
- 作為資質平庸的開發者,我需要打一點提前量,以免到時拙計
- 為了學習Java 8(主要是其中的 lambda 及相關庫),我先後閱讀了Oracle的 官方文件,Cay Horstmann(Core Java的作者)的 Java 8 for the Really Impatient 和Richard Warburton的 Java 8 Lambdas
- 但我感到並沒有多大收穫,Oracle的官方文件涉及了 lambda 表示式的每一個概念,但都是點到輒止;後兩本書(尤其是Java 8 Lambdas)花了大量篇幅介紹 Java lambda 及其類庫,但實質內容不多,讀完了還是沒有對Java lambda產生一個清晰的認識
- 關鍵在於這些文章和書都沒有解決我對Java lambda的困惑,比如:
- Java 8 中的 lambda 為什麼要設計成這樣?(為什麼要一個 lambda 對應一個介面?而不是 Structural Typing?)
- lambda 和匿名型別的關係是什麼?lambda 是匿名物件的語法糖嗎?
- Java 8 是如何對 lambda 進行型別推導的?它的型別推導做到了什麼程度?
- Java 8 為什麼要引入預設方法?
- Java 編譯器如何處理 lambda?
- 等等……
- 之後我在 Google 搜尋這些問題,然後就找到 Brian Goetz 的三篇關於Java lambda的文章(State of Lambda,State of Lambda libraries version 和 Translation of lambda),讀完之後上面的問題都得到了解決
- 為了加深理解,我決定翻譯這一系列文章
警告(Caveats)
如果你不知道什麼是函數語言程式設計,或者不瞭解 map
,filter
,reduce
這些常用的高階函式,那麼你不適合閱讀本文,請先學習函數語言程式設計基礎(比如 這本書)。
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.
關於
本文介紹了 Java SE 8 中新引入的 lambda 語言特性以及這些特性背後的設計思想。這些特性包括:
- lambda 表示式(又被成為“閉包”或“匿名方法”)
- 方法引用和構造方法引用
- 擴充套件的目標型別和型別推導
- 介面中的預設方法和靜態方法
1. 背景
Java 是一門面向物件程式語言。面向物件程式語言和函數語言程式設計語言中的基本元素(Basic Values)都可以動態封裝程式行為:面向物件程式語言使用帶有方法的物件封裝行為,函數語言程式設計語言使用函式封裝行為。但這個相同點並不明顯,因為Java 物件往往比較“重量級”:例項化一個型別往往會涉及不同的類,並需要初始化類裡的欄位和方法。
不過有些 Java 物件只是對單個函式的封裝。例如下面這個典型用例:Java API 中定義了一個介面(一般被稱為回撥介面),使用者通過提供這個介面的例項來傳入指定行為,例如:
1 2 3 |
public interface ActionListener { void actionPerformed(ActionEvent e); } |
這裡並不需要專門定義一個類來實現 ActionListener
,因為它只會在呼叫處被使用一次。使用者一般會使用匿名型別把行為內聯(inline):
1 2 3 4 5 |
button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ui.dazzle(e.getModifiers()); } }); |
很多庫都依賴於上面的模式。對於並行 API 更是如此,因為我們需要把待執行的程式碼提供給並行 API,並行程式設計是一個非常值得研究的領域,因為在這裡摩爾定律得到了重生:儘管我們沒有更快的 CPU 核心(core),但是我們有更多的 CPU 核心。而序列 API 就只能使用有限的計算能力。
隨著回撥模式和函數語言程式設計風格的日益流行,我們需要在Java中提供一種儘可能輕量級的將程式碼封裝為資料(Model code as data)的方法。匿名內部類並不是一個好的 選擇,因為:
- 語法過於冗餘
- 匿名類中的
this
和變數名容易使人產生誤解 - 型別載入和例項建立語義不夠靈活
- 無法捕獲非
final
的區域性變數 - 無法對控制流進行抽象
上面的多數問題均在Java SE 8中得以解決:
- 通過提供更簡潔的語法和區域性作用域規則,Java SE 8 徹底解決了問題 1 和問題 2
- 通過提供更加靈活而且便於優化的表示式語義,Java SE 8 繞開了問題 3
- 通過允許編譯器推斷變數的“常量性”(finality),Java SE 8 減輕了問題 4 帶來的困擾
不過,Java SE 8 的目標並非解決所有上述問題。因此捕獲可變變數(問題 4)和非區域性控制流(問題 5)並不在 Java SE 8的範疇之內。(儘管我們可能會在未來提供對這些特性的支援)
2. 函式式介面(Functional interfaces)
儘管匿名內部類有著種種限制和問題,但是它有一個良好的特性,它和Java型別系統結合的十分緊密:每一個函式物件都對應一個介面型別。之所以說這個特性是良好的,是因為:
- 介面是 Java 型別系統的一部分
- 介面天然就擁有其執行時表示(Runtime representation)
- 介面可以通過 Javadoc 註釋來表達一些非正式的協定(contract),例如,通過註釋說明該操作應可交換(commutative)
上面提到的 ActionListener
介面只有一個方法,大多數回撥介面都擁有這個特徵:比如 Runnable
介面和 Comparator
介面。我們把這些只擁有一個方法的介面稱為 函式式介面。(之前它們被稱為 SAM型別,即 單抽象方法型別(Single Abstract Method))
我們並不需要額外的工作來宣告一個介面是函式式介面:編譯器會根據介面的結構自行判斷(判斷過程並非簡單的對介面方法計數:一個介面可能冗餘的定義了一個 Object
已經提供的方法,比如 toString()
,或者定義了靜態方法或預設方法,這些都不屬於函式式介面方法的範疇)。不過API作者們可以通過 @FunctionalInterface
註解來顯式指定一個介面是函式式介面(以避免無意聲明瞭一個符合函式式標準的介面),加上這個註解之後,編譯器就會驗證該介面是否滿足函式式介面的要求。
實現函式式型別的另一種方式是引入一個全新的 結構化 函式型別,我們也稱其為“箭頭”型別。例如,一個接收 String
和Object
並返回 int
的函式型別可以被表示為 (String, Object) -> int
。我們仔細考慮了這個方式,但出於下面的原因,最終將其否定:
- 它會為Java型別系統引入額外的複雜度,並帶來 結構型別(Structural Type) 和 指名型別(Nominal Type) 的混用。(Java 幾乎全部使用指名型別)
- 它會導致類庫風格的分歧——一些類庫會繼續使用回撥介面,而另一些類庫會使用結構化函式型別
- 它的語法會變得十分笨拙,尤其在包含受檢異常(checked exception)之後
- 每個函式型別很難擁有其執行時表示,這意味著開發者會受到 型別擦除(erasure) 的困擾和侷限。比如說,我們無法對方法
m(T->U)
和m(X->Y)
進行過載(Overload)
所以我們選擇了“使用已知型別”這條路——因為現有的類庫大量使用了函式式介面,通過沿用這種模式,我們使得現有類庫能夠直接使用 lambda 表示式。例如下面是 Java SE 7 中已經存在的函式式介面:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.beans.PropertyChangeListener
除此之外,Java SE 8中增加了一個新的包:java.util.function
,它裡面包含了常用的函式式介面,例如:
Predicate<T>
——接收T
並返回boolean
Consumer<T>
——接收T
,不返回值Function<T, R>
——接收T
,返回R
Supplier<T>
——提供T
物件(例如工廠),不接收值UnaryOperator<T>
——接收T
物件,返回T
BinaryOperator<T>
——接收兩個T
,返回T
除了上面的這些基本的函式式介面,我們還提供了一些針對原始型別(Primitive type)的特化(Specialization)函式式介面,例如 IntSupplier
和 LongBinaryOperator
。(我們只為 int
、long
和 double
提供了特化函式式介面,如果需要使用其它原始型別則需要進行型別轉換)同樣的我們也提供了一些針對多個引數的函式式介面,例如 BiFunction<T, U, R>
,它接收 T
物件和 U
物件,返回 R
物件。
3. lambda表示式(lambda expressions)
匿名型別最大的問題就在於其冗餘的語法。有人戲稱匿名型別導致了“高度問題”(height problem):比如前面 ActionListener
的例子裡的五行程式碼中僅有一行在做實際工作。
lambda表示式是匿名方法,它提供了輕量級的語法,從而解決了匿名內部類帶來的“高度問題”。
下面是一些lambda表示式:
1 2 3 |
( int x, int y) -> x + y () -> 42 (String s) -> { System.out.println(s); } |
第一個 lambda 表示式接收 x
和 y
這兩個整形引數並返回它們的和;第二個 lambda 表示式不接收引數,返回整數 ‘42’;第三個 lambda 表示式接收一個字串並把它列印到控制檯,不返回值。
lambda 表示式的語法由引數列表、箭頭符號 ->
和函式體組成。函式體既可以是一個表示式,也可以是一個語句塊:
- 表示式:表示式會被執行然後返回執行結果。
- 語句塊:語句塊中的語句會被依次執行,就像方法中的語句一樣——
return
語句會把控制權交給匿名方法的呼叫者break
和continue
只能在迴圈中使用- 如果函式體有返回值,那麼函式體內部的每一條路徑都必須返回值
表示式函式體適合小型 lambda 表示式,它消除了 return
關鍵字,使得語法更加簡潔。
lambda 表示式也會經常出現在巢狀環境中,比如說作為方法的引數。為了使 lambda 表示式在這些場景下儘可能簡潔,我們去除了不必要的分隔符。不過在某些情況下我們也可以把它分為多行,然後用括號包起來,就像其它普通表示式一樣。
下面是一些出現在語句中的 lambda 表示式:
1 2 3 4 5 6 7 8 |
FileFilter java = (File f) -> f.getName().endsWith( "*.java"); String user = doPrivileged(() -> System.getProperty( "user.name")); new Thread(() -> { connectToService(); sendNotification(); }).start(); |
4. 目標型別(Target typing)
需要注意的是,函式式介面的名稱並不是 lambda 表示式的一部分。那麼問題來了,對於給定的 lambda 表示式,它的型別是什麼?答案是:它的型別是由其上下文推導而來。例如,下面程式碼中的 lambda 表示式型別是 ActionListener
:
1 |
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers()); |
這就意味著同樣的 lambda 表示式在不同上下文裡可以擁有不同的型別:
1 2 3 |
Callable<String> c = () -> "done"; PrivilegedAction<String> a = () -> "done"; |
第一個 lambda 表示式 () -> "done"
是 Callable
的例項,而第二個 lambda 表示式則是 PrivilegedAction
的例項。
編譯器負責推導 lambda 表示式型別。它利用 lambda 表示式所在上下文 所期待的型別 進行推導,這個 被期待的型別 被稱為 目標型別。lambda 表示式只能出現在目標型別為函式式介面的上下文中。
當然,lambda 表示式對目標型別也是有要求的。編譯器會檢查 lambda 表示式的型別和目標型別的方法簽名(method signature)是否一致。當且僅當下面所有條件均滿足時,lambda 表示式才可以被賦給目標型別 T
:
T
是一個函式式介面- lambda 表示式的引數和
T
的方法引數在數量和型別上一一對應 - lambda 表示式的返回值和
T
的方法返回值相相容(Compatible) - lambda 表示式內所丟擲的異常和
T
的方法throws
型別相相容
由於目標型別(函式式介面)已經“知道” lambda 表示式的形式引數(Formal parameter)型別,所以我們沒有必要把已知型別再重複一遍。也就是說,lambda 表示式的引數型別可以從目標型別中得出:
1 |
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2); |
在上面的例子裡,編譯器可以推匯出 s1
和 s2
的型別是 String
。此外,當 lambda 的引數只有一個而且它的型別可以被推導得知時,該引數列表外面的括號可以被省略:
1 2 3 |
FileFilter java = f -> f.getName().endsWith(
".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));
|
這些改進進一步展示了我們的設計目標:“不要把高度問題轉化成寬度問題。