Java Lambda表達式初探
前言
本文受啟發於Trisha Gee在JavaOne 2016的主題演講Refactoring to Java 8。
Java 8已經發行兩年多,但很多人仍然在使用JDK7。對企業來說,技術上謹慎未必是壞事,但對個人學習而言,不去學習新科技就很可能被科技拋棄。 Java 8一個重要的變更是引入Lambda表達式(lambda expression),這聽起來似乎很牛,有種我雖然不知道Lambda表達式是什麼,但我仍然覺得很厲害的感覺。不要害怕,具體到語言層面上Lambda表達式不過是一種新的語法而已,有了它,Java將開啟函數式程式設計的大門。
為什麼需要Lambda表達式
不要糾結什麼是Lambda表達式、什麼是函數式程式設計。先來看看Java 8新的語法特性帶來的便利之處,相信你會過目不忘的。
在有Lambda表達式之前,要新建一個線程,需要這樣寫:
new Thread(new Runnable(){ @Override public void run(){ System.out.println("Thread run()"); } }).start();
有Lambda表達式之後,則可以這樣寫:
new Thread( () -> System.out.println("Thread run()") ).start();
正如你所見,之前無用的模板代碼不見了!如上所示,Lambda表達式一個常見的用法是取代(某些)匿名內部類,但Lambda表達式的作用不限於此。
Lambda表達式的原理
剛接觸Lambda表達式可能覺得它很神奇:不需要宣告類別或方法的名字,就可以直接定義函數。但其實這只是編譯器給我們的一個小把戲,背後的原理並不難理解。以下是Lambda表達式幾種可能的書寫形式:
Runnable run = () -> System.out.println("Hello World");// 1ActionListener listener = event -> System.out.println("button clicked");// 2Runnable multiLine = () -> {// 3 System.out.println("Hello "); System.out.println("World"); }; BinaryOperator<Long> add = (Long x, Long y) -> x + y;// 4BinaryOperator<Long> addImplicit = (x, y) -> x + y;// 5
透過上例可以發現:
Lambda表達式是有類型的,賦值運算的左邊就是型別。 Lambda表達式的類型其實是對應介面的類型。
Lambda表達式可以包含多行程式碼,需要用大括號把程式碼區塊括起來,就像寫函數體一樣。
大多數時候,Lambda表達式的參數表可以省略型別,就像程式碼2和5一樣。這得歸功於javac的類型推導機制,編譯器可以根據上下文推導出類型資訊。
其實每個Lambda表達式都是原來匿名內部類別的簡寫形式,該內部類別實作了某個函數介面(Functional Interface)。所謂函數介面是指*新增了@FunctionalInterface標註,且內部只有一個介面函數的介面*。 Java是強類型語言,無論有沒有明確指明,每個變數和物件都必須有明確的類型,沒有明確指定的時候編譯器會嘗試確定類型。 Lambda表達式的類型就是對應函數介面的類型。
Lambda表達式和Stream
Lambda表達式的另一個重要用法,是和Stream一起使用。 Stream is a sequence of elements supporting sequential and parallel aggregate operations。 Stream就是一組元素的序列,支援對這些元素進行各種操作,而這些操作是透過Lambda表達式指定的。可以把Stream看作Java Collection的一種視圖,就像迭代器是容器的一種視圖那樣(但Stream不會修改容器中的內容)。下面例子展示了Stream的常見用法。
範例1
假設需要從一個字串清單中選出以數字開頭的字串並輸出,Java 7之前需要這樣寫:
List<String> list = Arrays.asList("1one", "two", "three", "4four");for(String str : list){ if(Character.isDigit(str.charAt(0))){ System.out.println(str); } }
而Java 8就可以這樣寫:
List<String> list = Arrays.asList("1one", "two", "three", "4four"); list.stream()// 1.得到容器的Steam .filter(str -> Character.isDigit(str.charAt(0)))// 2.选出以数字开头的字符串 .forEach(str -> System.out.println(str));// 3.输出字符串
上述程式碼首先1 . 呼叫List.stream()方法得到容器的Stream,2. 然後呼叫filter()方法過濾出以數字開頭的字串,3. 最後呼叫forEach()方法輸出結果。
使用Stream有兩個明顯的好處:
減少了模板程式碼,只用Lambda表達式指明所需操作,程式碼語意更加明確、方便閱讀。
將外部迭代改成了Stream的內部迭代,方便了JVM本身對迭代過程做最佳化(例如可以並行迭代)。
例子2
假設需要從一個字串清單中,選出所有不以數字開頭的字串,將其轉換成大寫形式,並把結果放到新的集合當中。 Java 8書寫的程式碼如下:
List<String> list = Arrays.asList("1one", "two", "three", "4four"); Set<String> newList = list.stream()// 1.得到容器的Stream .filter(str -> !Character.isDigit(str.charAt(0)))// 2.选出不以数字开头的字符串 .map(String::toUpperCase)// 3.转换成大写形式 .collect(Collectors.toSet());// 4.生成结果集
上述代码首先1. 调用List.stream()方法得到容器的Stream,2. 然后调用filter()方法选出不以数字开头的字符串,3. 之后调用map()方法将字符串转换成大写形式,4. 最后调用collect()方法将结果转换成Set。这个例子还向我们展示了方法引用(method references,代码中标号3处)以及收集器(Collector,代码中标号4处)的用法,这里不再展开说明。
通过这个例子我们看到了Stream链式操作,即多个操作可以连成一串。不用担心这会导致对容器的多次迭代,因为不是每个Stream的操作都会立即执行。Stream的操作分成两类,一类是中间操作(intermediate operations),另一类是结束操作(terminal operation),只有结束操作才会导致真正的代码执行,中间操作只会做一些标记,表示需要对Stream进行某种操作。这意味着可以在Stream上通过关联多种操作,但最终只需要一次迭代。如果你熟悉Spark RDD,对此应该并不陌生。
结语
Java 8引入Lambda表达式,从此打开了函数式编程的大门。如果你之前不了解函数式编程,不必纠结于这个概念。编程过程中简洁明了的书写形式以及强大的Stream API会让你很快熟悉Lambda表达式的。
本文只对Java Lambda表达式的基本介绍,希望能够激发读者对Java函数式编程的兴趣。如果本文能够让你觉得Lambda表达式很好玩,函数式编程很有趣,并产生了进一步学习的欲望,那就再好不过了。文末参考文献中列出了一些有用的资源。
参考文献
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html
http://www.slideshare.net/trishagee/refactoring-to-java-8-devoxx-uk
《Java 8函数式编程 [英]沃伯顿》
https://www.oracle.com/javaone/speakers.html#gee