Java 8 Lambda新語法,簡化程式,增強效能


前些日子,Java 8在Oracle網頁上悄聲發佈啦!比較重要的新功能是,Java 8導入了一個新型態的語法-Lambda。然而這個Lambda語法並不是新的語法,在Script Languages和Functional Languages中都可以常常見到,為什麼Java 8要特地導入Lambda呢?難道先前的那些語法沒辦法完成什麼特別的功能嗎?這問題待文章之後解答,我們先來看看什麼是Lambda

什麼是Lambda

Lambda通用定義

在不同領域,對於Lambda的定義可能不太相同,但概念都是Lambda為一個函數,可以根據輸入的值,決定輸出的值。但Lambda與一般函數不同的是,Lambda並不需要替函數命名(如F(X) = X + 2, G(X) = F(X) + 3中的F、G便是函數的名稱),所以我們常常把Lambda形容為「匿名的」(Anonymous)。

匿名類別(Anonymous class)

在Java中,匿名類別是非常常見的物件實體化方式,相信學過Java的人都有用過類似以下的方式去實體化一個物件出來:

以上是將匿名類別實體化出來的一段程式。為何說程式碼內的ClassA是匿名的呢?其實匿名類別指的不是ClassA(ClassA已有「ClassA」這個名稱,自然不會是一個匿名類別),而是在new的同時,被重新修改過的ClassA(在new statment之後加上大括號「{}」,就判定為重新修改)。這樣說可能還是很模糊,如果對照以下非匿名類別的撰寫方式,就可以清楚匿名和非匿名的差異了。

將原先使用的匿名類別,另外宣告成一個ClassB出來,調用時直接new ClassB,此處的類別便不是匿名類別,而相比每次只能夠在同一個程式碼位置new出實體的匿名類別,ClassB則可以在任何Scope所及的地方new出實體。

我們都知道,Java會把每個類別在編譯時期,都創建出相對應的.class檔案。如果是一般的類別,則會直接以Class的名稱作為.class檔案的檔名。如果是內部(Inner)的非匿名類別,則會以$的分隔字元,表示類別在哪層類別之下,作為.class檔案的檔名。舉例,編譯出以下的程式,會得到A.class與A$B.class兩個檔案。

但如果有內部的匿名類別,那.class會如何產生呢?無論匿名類別是從何種類別重新修改而來,Java都會按照匿名類別是在哪層底下,給予流水的數字編號。舉例,編譯出以下的程式,會得到C.class、A.class、A$B.class、A$1.class與A$2.class五個檔案。

由此可知,匿名類別實際上也擁有一個.class檔案,在概念上,它其實就是將一個類別或是介面在編譯new statement時由編譯器重新產生出另一個類別。

JavaLambda

Java 8之後,導入了Lambda語法,或者可以稱為Lambda表示式。如果說Lambda語法是用來表示一個匿名類別的話,那就不太正確了。實際上Lambda語法只能用來表示一個「只擁有一個方法的介面」所實作出來的匿名類別,但這樣講也是不太正確,在文章之後會進行觀念的改正,現在姑且先將其認知成這樣。

「只擁有一個方法的介面」在Java中很常使用到,例如執行緒的Runnable介面只擁有一個run方法,或是AWT的ActionListener只擁有一個actionPerformed方法。這類介面,都可以直接使用Lambda,將它們擁有的那單個方法快速實作出來。在Java 8之前,我們將這類的介面稱為Single Abstract Method type(SAM);在Java 8之後,因為這類的介面變得十分重要,所以將其另稱為Functional Interface

Lambda語法結構

input -> body

其中,input和body都各有多種撰寫方式,以下分別舉例。

input

不輸入

單個輸入

多個輸入(不省略型態)

多個輸入(省略型態)

body

什麼都不做

單行不回傳值

多行不回傳值

單行回傳值

多行回傳值

以上幾種方式,可以自己練習組合看看。另外還有一種特殊語法結構,可以省略掉input和->,因為會使程式碼較不易閱讀,不建議使用,將在文章最後提到。

Lambda實際應用與效能比較

取代Functional Interface產出的匿名類別

在Java中,許多只有一個方法的介面,如果要使用這些介面,往往需要使用到至少4行程式碼才有辦法達成,舉例:

這樣的使用方式,往往會讓Java程式拉的很長,變得十分複雜。因此Lambda最重要的功能,就是取代Functional Interface所產出的匿名類別,簡化程式碼,甚至還可以提升效能(稍候提到)。舉例,同樣的Runnable使用方式,可以減化成:

使用Lambda來取代以往Functional Interface的使用方式,可以大大的縮短程式碼,在編譯的過程中,也可以避免掉產生新的.class檔案出來,執行的時候,也不會再重新new出一個物件實體,而是直接將Lambda的body程式碼存放在記憶體,直接以類似call function的方式去執行,大大的提升程式的執行效能。

如果有興趣知道Lambda是如何運作,可以查看編譯出來.class檔案的bytecode(),查看命令如下:

javap -c Class路徑

舉例:

javap -c org.magiclen.A

比較原先不使用Lambda,與使用Lambda的方式:

不使用Lambda

使用Lambda

可以發現使用Lambda就連bytecode,都可以從3行命令,縮減成一行命令。invokedynamic就是動態調用記憶體內的某段程式碼。

此外,因為少了new這個指令,使用Lambda實作出來的方法,並不會另外實體化出物件,因此會有以下這個現象發生:

雖然兩個執行緒都是呼叫this.getClass(),但print出來的結果卻不一樣。而且可以知道使用Lambda的r1,this所指的物件就是此行Lambda語法所在的物件,並不會像沒使用Lambda的r2一樣變成一個匿名類別的物件。

LambdaCollection的關係

Lambda除了用來取代以往使用Functional Interface的方式外,還可以用在Collection的走訪、過濾和一些簡單的運算上。而且同樣地,使用Lambda可以增加不少的效能。

走訪
過去我們使用For each走訪一個List可能要寫成這樣:

如果要走訪一個Map更麻煩,可能要寫成這樣:

若改以Lambda來走訪這些Collection,可以簡化成這樣:

程式碼精簡許多,因為少了迴圈,就連bytecode也能將原先十幾行指令省略到剩下三行。

過濾和基本運算

在新的Java 8中,Collection提供了stream()方法,可以對集合做一些過濾和基本運算,而且這個當然也是有經過效能優化過的。除了stream()方法外還有parallelStream()方法,可以讓Collection各別針對它的entry另開出一個Thread,進行stream提供的運算,讓多核心的CPU資源更能有效的被利用。以下將舉幾個過濾和基本運算的例子:

以上會輸出「12」。

以上會輸出「127」。

以上會輸出

15
1.5
」。

Lambda特殊精簡語法結構

除了前面介紹到的input -> body結構,Lambda在某些特定的場合下,還能夠寫出更短的語法,結構如下:

方法名稱

什麼!?只要寫方法名稱就好了嗎?甚至連參數也不用傳!是的,Lambda允許這種類型的語法存在,但必須要明確指定方法名稱是在哪個類別或是哪個物件之下,而且最後一個「.」要改成「::」。這樣說好像很模糊,舉幾個例子好了:

以上輸出結果為:

嗨嗨
哈囉
6.256.25

這樣的結構雖然可以使得程式變得非常精簡,卻也不易閱讀。這種撰寫方式,實際上常與forEach搭配,如下:

以上程式會輸出「12354」。

總結

Java 8依靠Lambda新語法,簡化程式,改善效能,在結構上淺顯易懂、容易使用,實屬不錯的新功能!

關於作者

Magic Len

各位好,我是Magic Len,是這網站的管理員。我是台灣台中大肚山上人,畢業於台中高工資訊科和台灣科技大學資訊工程系,曾在桃機航警局服役。我熱愛自然也熱愛科學,喜歡和別人分享自己的知識與經驗。如果你有興趣認識我,可以加我的Facebook,並且請註明是從MagicLen來的。

相關文章