
認識 Lambda/Closure(5)Java 的稻草人提案
認識 Lambda/Closure(4)從 Scala 中借鏡 << 前情 終於要開始討論 Java 的 Lambda/Closure 了!不過這邊會先討論 2009 年提出的 舊草案,討論這份舊草案,有助於我們瞭解為什麼 Lambda/Closure 會演變至今天 JDK8 所採取的形式。 如果打算對一列整數排序,在 JDK8 之前,你也許會寫下以下的程式碼: // asList 與 sort 方法是從 Arrays 與 Collections 中 static import 而來 List<Integer> numbers = asList(3, 2, 6, 4); sort(numbers, new Comparator<Integer>() { public int compare(Integer n1, Integer n2) { return -n1.compareTo(n2); } }); 你必須告訴 List<Integer> numbers = asList(3, 2, 6, 4); Comparator<Integer> descending = new Comparator<Integer>() { public int compare(Integer n1, Integer n2) { return -n1.compareTo(n2); } }; sort(numbers, descending); 現在,我們可以清楚地看出打算令 List<Integer> numbers = asList(3, 2, 6, 4); sort(numbers, (n1, n2) -> -n1.compareTo(n2)); 在 Java 中,匿名類別是最類似 Lambda/Closure 的東西,這也是有些人聲稱 Java 其實不需要 Lambda/Closure 的原因。基本上,這沒有錯,只是在某些場合中,我們得寫比較多的程式碼罷了。近幾年來,撰寫簡明程式碼越來越被重視。雖然使用沒有 Lambda/Closure 的 Java,還是可以寫出你想要的功能,使用 Lambda/Closure 卻可以寫出簡潔的程式碼,你或其他人在讀取這樣的程式碼時會有助於產能。就如同 Bob Martin 大叔在《Clean Code》書中談到的: 今日你撰寫程式碼的難易度,取決於其周遭程式碼閱讀時的難易度。 匿名類別冗長的語法不是唯一的問題。如果匿名類別打算捕捉區域變數的話,該變數必須被宣告為 public static FactorProducer createFactorProducer(max) { final int[] primes = ...; FactorProducer producer = new FactorProducer() { public int factor() { ... while(pow(primes[i], 2)) { ... } } }; return producer; } 在 Java 中,區域變數的生命週期有別於物件。一旦方法執行完畢,所有區域變數的生命週期也就結束了。如果匿名類別的實例能確實捕捉區域變數,並從方法中傳回,當你透過該實例存取到已結束生命週期的區域變數時會如何?為了避免這類問題,如果區域變數會在匿名類別中使用的話,Java 編譯器強迫你要在區域變數上加上 匿名類別中的 var sum = 0; [1, 2, 3, 4, 5].forEach(function(elem) { sum += elem; }); 可寫的閒置變數也代表著狀態是可變的(Mutable),在並行(Concurrent)程式設計時就得處理鎖定(Locking)問題。為了避免處理複雜的變數生命週期以及並行問題,如之後文章我們將看到的,JDK8 特意禁止捕捉可變的區域變數。 在 2009 年的一份草案中,要定義 Lambda,以及要宣告一個可接受 Lambda 的變數,會是像這樣: #int(int) doubler = #(int x)(2 * x); doubler.(3) // 呼叫 Lambda 以上範例作用上類似於以下: int doubler(int x) { return 2 * x; } doubler(3); 具備兩個 #int(int, int) sum = #(int x, int y)(x + y) 以上程式碼在作用上類似於以下: int sum(int x, int y) { return x + y; } 如果要用這個語法來寫一下 認識 Lambda/Closure(四) 中的 def bubbleSort(int[] arr, #boolean(int, int) order) { ... boolean o = order.(a, b); ... } int[] arr = new int[] {2, 5, 1, 7, 8}; bubbleSort(arr, #(int a, int b)(a > b)); 這邊的重點在於,舊草案要求接受 Lambda 的變數,必須宣告函式型態。可以看出宣告函式型態的語法中,傳回值型態是放在左邊,然而定義 Lambda 時,函式本體是放在右邊。來思考一個問題,如果你有個 Lambda 會傳回 Lambda,那麼函式型態宣告會長什麼樣子? ##int(int)(int) sum = #(int x)(#(int y)(x + y)); 哇喔…這是 C/C++ 的指標嗎?另一個問題在於,如果必須為了 Lambda/Closure 而宣告函式型態,那麼就得為 Lambda/Closure 建立一套新的 API。現有的 API 沒辦法直接受惠於新引入的 Lambda/Closure,更何況,還得解決涉及到泛型時的複雜問題。 幸運地,JDK8 沒有採取這種特定函式型態的語法,它使用單一抽象方法(Single abstract method)型態,也就是之後被稱為函式介面(Functional interface)的方式,而這是之後的文章中將要探討的內容。 |