認識 Lambda/Closure(7)JDK8 Lambda 語法
認識 Lambda/Closure(6)一級函式與 Lambda 演算 << 前情 你可以在 Google Play 或 Pubu 購買 Java Lambda Tutorial 系列文章的電子書。 終於要來介紹 JDK8 Lambda 語法了。在 JDK8 中要表示 (Integer x) -> x + 2 別忘了,Java 是靜態定型語言,所以在這邊型態宣告是必要的。在 認識 Lambda/Closure(4) 中我們談過,對靜態定型語言來說,類型推斷(Type inference)對採用 Lambda/Closure 時是很重要的特性。當然,JDK8 必然得提供更強的類型推斷,如此在某些場合中,就可以省略型態宣告。先別急,本文介紹才剛開始。即便如此,以上的語法比起 認識 Lambda/Closure(6) 中的匿名類別來說,語法上還是簡潔許多了。 有了 JDK8 Lambda,要寫個 public static <A, B, C> Func<A, C> compose(Func<A, B> f, Func<B, C> g) { return x -> g.apply(f.apply(x)); } 我們仍然需要 認識 Lambda/Closure(6) 中定義的 compose((Integer x) -> x + 2, (Integer y) -> y * 3); 以下再次列出 認識 Lambda/Closure(6) 中使用匿名類別的寫法,比較一下兩個程式碼,你會選擇使用哪個呢? compose( new Func<Integer, Integer>() { public Integer apply(Integer x) { return x + 2; } }, new Func<Integer, Integer>() { public Integer apply(Integer y) { return y * 3; } } ); JDK8 Lambda 的通用語法是由參數列、箭號 // 計算 x 與 y 的和 (int x, int y) -> x + y // 不帶引數,直接傳回整數 42 () -> 42 在 JDK8 Lambda 中,本體可以是單一運算式或者是陳述區塊。例如: // 取得字串並輸出至主控台,沒有傳回值 (String s) -> { out.println(s); } // 取一個整數並傳回一個整數 (Integer x) -> { Integer result; ...other statements ... return result; }; 區塊可以由數個陳述句組成,不過基本上不建議如此使用。在運用 Lambda 時,儘量使用簡單的運算式會是比較好的。如果你的實作比較複雜,還有其他方式可以運用到 JDK8 Lambda 的好處,之後的文章就會介紹到。 在一些語言中,Lambda 表示式本身就具備型態,像是 JavaScript 中匿名函式會是 public interface Runnable { void run(); } public interface Callable<V> { V call() throws Exception; } public interface Comparator<T> { int compare(T o1, T o2); } 在 認識 Lambda/Closure(6) 中定義的 Func<Integer, Integer> func = x -> x * 2; 在這邊型態推斷發揮了作用,參數 public interface Function<P, R> { R call(P p); } 在以下的範例中,同樣是 Function<Double, Double> f2 = x -> x * 2; 所以,Lambda 表示式本身是中性的,它本身無關乎函式介面的名稱,它只關心方法簽署,但忽略方法名稱。 函式介面是僅具單一抽象方法的介面,不過有時候會難以直接看出介面是否為函式介面。例如,介面可能有預設方法(JDK8 的新特性)、可能繼承其他介面、重新定義了某些方法等,這些都會使得確認介面是否為函式介面更為困難。有個新的標註 @FunctionalInterface public interface Func<P, R> { R apply(P p); } 如果介面使用了 @FunctionalInterface public interface Function<P, R> { R call(P p); R call(P p1, P p2); } 編譯器會對此介面產生以下編譯錯誤: >@FunctionalInterface 看來來,Lambda 語法不過就是匿名類別的編譯器語法蜜糖嘛!真的嗎?來看一下接下來的程式,想想看結果會如何顯示? import static java.lang.System.out; public class Hello { Runnable r1 = new Runnable() { public void run() { out.println(this); } }; Runnable r2 = new Runnable() { public void run() { out.println(toString()); } }; public String toString() { return "Hello, world!"; } public static void main(String[] args) { new Hello().r1.run(); new Hello().r2.run(); } } 結果會顯示像是 Hello$1@103368e 與 Hello$2@1f2ae62,這是因為 import static java.lang.System.out; public class Hello { Runnable r1 = () -> { out.println(this); }; Runnable r2 = () -> { out.println(toString()); }; public String toString() { return "Hello, world!"; } public static void main(String[] args) { new Hello().r1.run(); new Hello().r2.run(); } } 結果會顯示兩次的 “Hello, world!",也就是說,Lambda 表示式本體中的 不過,我們可以在 Lambda 表示式中改變被捕捉的變數值嗎?像是在 JavaScript 或 Scala 中可以做到的事情?因為可重新指定的閒置變數(Free variable)也代表著可變的狀態,而可變狀態代表著在並行程式設計(JDK8 會想要採用 Lambda 的理由之一)會有鎖定問題,JDK8 特意禁止你捕捉可變動的區域變數。你無法在 Lambda 表示式中改變被捕捉的變數值。 你已經看過 JDK8 Lambda 的基本語法了,如你到目前看到的,與現有 API 保持相容性也是 Java 中採用 Lambda 的目標之一。Java 是一門古老且具有一大堆 API 的語言,它會採用什麼策略來解決這個問題?這是下一篇文章所要討論的內容。 |