1. 程式人生 > >Java Lambda 表示式初探

Java Lambda 表示式初探

前言

Java 8已經發行兩年多,但很多人仍然在使用JDK7。對企業來說,技術上謹慎未必是壞事,但對個人學習而言,不去學習新技術就很可能被技術拋棄。Java 8一個重要的變更是引入Lambda表示式(lambda expression),這聽起來似乎很牛,有種我雖然不知道Lambda表示式是什麼,但我仍然覺得很厲害的感覺。不要怕,具體到語言層面上Lambda表示式不過是一種新的語法而已,有了它,Java將開啟函數語言程式設計的大門。

為什麼需要Lambda表示式

不要糾結什麼是Lambda表示式、什麼是函數語言程式設計。先來看一下Java 8新的語法特性帶來的便利之處,相信你會過目不忘的。

在有Lambda

表示式之前,要新建一個執行緒,需要這樣寫:

123456 newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("Thread run()"
);}}).start();

Lambda表示式之後,則可以這樣寫:

123 newThread(()->System.out.println("Thread run()")).start();

正如你所見,之前無用的模板程式碼不見了!如上所示,Lambda表示式一個常見的用法是取代(某些)匿名內部類

,但Lambda表示式的作用不限於此。

Lambda表示式的原理

剛接觸Lambda表示式可能覺得它很神奇:不需要宣告類或者方法的名字,就可以直接定義函式。這看似是編譯器為匿名內部類簡寫提供的一個小把戲,但事實上並非如此,Lambda表示式實際上是通過invokedynamic指令來實現的。先別管這麼多,下面是Lambda表示式幾種可能的書寫形式,“看起來”並不是很難理解。

12345678 Runnable run=()->System.out.println("Hello World");// 1ActionListener listener=event->System.out.println("button clicked");// 2Runnable multiLine=()->{// 3System.out.println("Hello ");System.out.println("World");};BinaryOperator<Long>add=(Longx,Longy)->x+y;// 4BinaryOperator<Long>addImplicit=(x,y)->x+y;// 5

通過上例可以發現:

  • Lambda表示式是有型別的,賦值操作的左邊就是型別。Lambda表示式的型別實際上是對應介面的型別
  • Lambda表示式可以包含多行程式碼,需要用大括號把程式碼塊括起來,就像寫函式體那樣。
  • 大多數時候,Lambda表示式的引數表可以省略型別,就像程式碼2和5那樣。這得益於javac的型別推導機制,編譯器可以根據上下文推匯出型別資訊。

表面上看起來每個Lambda表示式都是原來匿名內部類的簡寫形式,該內部類實現了某個函式介面(Functional Interface),但事實比這稍微複雜一些,這裡不再展開。所謂函式介面是指內部只有一個介面函式的介面。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之前需要這樣寫:

123456 List<String>list=Arrays.asList("1one","two","three","4four");for(Stringstr:list){if(Character.isDigit(str.charAt(0))){System.out.println(str);}}

而Java 8就可以這樣寫:

1234 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有兩個明顯的好處:

  1. 減少了模板程式碼,只用Lambda表示式指明所需操作,程式碼語義更加明確、便於閱讀。
  2. 將外部迭代改成了Stream的內部迭代,方便了JVM本身對迭代過程做優化(比如可以並行迭代)。

例子2

假設需要從一個字串列表中,選出所有不以數字開頭的字串,將其轉換成大寫形式,並把結果放到新的集合當中。Java 8書寫的程式碼如下:

123456 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表示式很好玩,函數語言程式設計很有趣,併產生了進一步學習的慾望,那就再好不過了。文末參考文獻中列出了一些有用的資源。

參考文獻