Java 8 | Lambda 新語法,簡化程式,增強效能
以下文章內容是參考了Java 8 Lambda新語法,簡化程式,增強效能,這篇文章,我參考很多文章後,發現這篇文章對我來說是最清晰最容易了解;而且文章中不單單只是說明了Java 8
的特性,還提到了相關的Anonymous class
,這讓我感到驚艷,所以特別挑選此篇文章來做學習。
Lambda 通用定義
在不同領域,對於Lambda
的定義可能不太相同,但概念都是Lambda
為一個函數,可以根據輸入的值,決定輸出的值。但Lambda
與一般函數不同的是,Lambda
並不需要替函數命名(如F(X) = X + 2, G(X) = F(X) + 3中的F、G便是函數的名稱),所以我們常常把Lambda
形容為「匿名的」(Anonymous
)。
匿名類別 (Anonymous class)
在Java
中,匿名類別是非常常見的物件實體化方式,類似以下的方式去實體化一個物件出來:
1 | class ClassA { |
以上是將匿名類別實體化出來的一段程式。為何說程式碼內的ClassA
是匿名的呢?其實匿名類別指的不是ClassA
(ClassA
已有「ClassA
」這個名稱,自然不會是一個匿名類別),而是在new
的同時,被重新修改過的ClassA
(在new statment
之後加上大括號「{}」,就判定為重新修改)。這樣說可能還是很模糊,如果對照以下非匿名類別的撰寫方式,就可以清楚匿名和非匿名的差異了。
1 | class ClassA { |
將原先使用的匿名類別,另外宣告成一個ClassB
出來,調用時直接new ClassB
,此處的類別便不是匿名類別,而相比每次只能夠在同一個程式碼位置new
出實體的匿名類別,ClassB
則可以在任何Scope
所及的地方new
出實體。
我們都知道,Java
會把每個類別在編譯時期,都創建出相對應的.class
檔案。如果是一般的類別,則會直接以Class
的名稱作為.class
檔案的檔名。如果是內部(Inner
)的非匿名類別,則會以$
的分隔字元,表示類別在哪層類別之下,作為.class
檔案的檔名。舉例,編譯出以下的程式,會得到A.class
與A$B.class
兩個檔案。
1 | class A { |
但如果有內部的匿名類別,那.class
會如何產生呢?無論匿名類別是從何種類別重新修改而來,Java
都會按照匿名類別是在哪層底下,給予流水的數字編號。舉例,編譯出以下的程式,會得到C.class
、A.class
、A$B.class
、A$1.class
與A$2.class
五個檔案。
1 | class C{ |
由此可知,匿名類別實際上也擁有一個.class
檔案,在概念上,它其實就是將一個類別或是介面在編譯new statement
時由編譯器重新產生出另一個類別。
Java 的 Lambda
在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 語法結構
1 | input -> body |
其中,input
和body
都各有多種撰寫方式,以下分別舉例。
input
- 不輸入
1 | () |
- 單個輸入
1 | x |
- 多個輸入(不省略型態)
1 | (int x,int y) |
- 多個輸入(省略型態)
1 | (x,y) |
body
- 什麼都不做
1 | {} |
- 單行不回傳值
1 | System.out.println("NO"); |
- 多行不回傳值
1 | { |
- 單行回傳值
1 | x+y |
- 多行回傳值
1 | { |
另外還有一種特殊語法結構,可以省略掉input
和->
,因為會使程式碼較不易閱讀,不建議使用。
Lambda 實際應用與效能比較
取代Functional Interface產出的匿名類別
在Java
中,許多只有一個方法的介面,如果要使用這些介面,往往需要使用到至少4
行程式碼才有辦法達成,舉例:
1 | Runnable runnbale = new Runnable() { |
這樣的使用方式,往往會讓Java
程式拉的很長,變得十分複雜。因此Lambda
最重要的功能,就是取代Functional Interface
所產出的匿名類別,簡化程式碼,甚至還可以提升效能。舉例,同樣的Runnable
使用方式,可以減化成:
1 | Runnable runnbale = () -> System.out.println("run me!"); |
使用Lambda
來取代以往Functional Interface
的使用方式,可以大大的縮短程式碼,在編譯的過程中,也可以避免掉產生新的.class
檔案出來,執行的時候,也不會再重新new
出一個物件實體,而是直接將Lambda
的body
程式碼存放在記憶體,直接以類似call function
的方式去執行,大大的提升程式的執行效能。
如想知道Lambda
是如何運作,可以查看編譯出來.class
檔案的bytecode()
,查看命令如下:
1 | javap -c Class路徑 |
舉例:
1 | javap -c com.morose.A |
比較原先不使用Lambda
,與使用Lambda
的方式:
不使用Lambda
1 | 0: new #2 |
使用Lambda
1 | 0: invokedynamic #2, 0 |
可以發現使用Lambda
就連bytecode
,都可以從3
行命令,縮減成一行命令。invokedynamic
就是動態調用記憶體內的某段程式碼。
註:如果想了解什麼是bytecode,可以參考此篇文章機器碼(machinecode)和字節碼(bytecode)是什麼?。
此外,因為少了new
這個指令,使用Lambda
實作出來的方法,並不會另外實體化出物件,因此會有以下這個現象發生:
1 | Runnable r1 = () -> System.out.println("r1: " + this.getClass()); |
雖然兩個執行緒都是呼叫this.getClass()
,但print出來的結果卻不一樣。而且可以知道使用Lambda
的r1
,this
所指的物件就是此行Lambda
語法所在的物件,並不會像沒使用Lambda
的r2
一樣變成一個匿名類別的物件。
Lambda 與 Collection 的關係
Lambda
除了用來取代以往使用Functional Interface
的方式外,還可以用在Collection
的走訪、過濾和一些簡單的運算上。而且同樣地,使用Lambda
可以增加不少的效能。
走訪
過去我們使用For each
走訪一個List
可能要寫成這樣:
1 | for (String s : list) { |
如果要走訪一個Map
更麻煩,可能要寫成這樣:
1 | Set<String> keySet = map.keySet(); |
若改以Lambda
來走訪這些Collection
,可以簡化成這樣:
1 | list.forEach(s -> System.out.print(s)); |
1 | map.forEach((k, v) -> System.out.print(k + ":" + v)); |
程式碼精簡許多,因為少了迴圈,就連bytecode
也能將原先十幾行指令省略到剩下三行。
過濾和基本運算
在新的Java 8
中,Collection
提供了stream()
方法,可以對集合做一些過濾和基本運算,而且這個當然也是有經過效能優化過的。除了stream()
方法外還有parallelStream()
方法,可以讓Collection
各別針對它的entry
另開出一個Thread
,進行stream
提供的運算,讓多核心的CPU
資源更能有效的被利用。以下將舉幾個過濾和基本運算的例子:
1 | // 原寫法 |
1 | // 使用stream |
以上輸出的結果皆為:12。
1 | // 原寫法 |
1 | // 使用stream |
以上輸出的結果皆為:127。
1 | // 原寫法 |
1 | // 使用stream |
以上輸出的結果皆為:
1 | 15 |
Lambda 特殊精簡語法結構
注:文章作者有提到這樣的結構雖然可以使得程式變得非常精簡,卻也不易閱讀。
除了前面介紹到的input -> body
結構,Lambda
在某些特定的場合下,還能夠寫出更短的語法,結構如下:
1 | 方法名稱 |
什麼!?只要寫方法名稱就好了嗎?甚至連參數也不用傳!是的,Lambda
允許這種類型的語法存在,但必須要明確指定方法名稱是在哪個類別或是哪個物件之下,而且最後一個「.」要改成「::」。這樣說好像很模糊,舉幾個例子好了:
1 | interface B { |
以上輸出的結果為:
1 | 嗨嗨 |
這種撰寫方式,實際上常與forEach
搭配,如下:
1 | List<String> list = new ArrayList<String>(); |
以上輸出的結果為:12354。
總結
在這篇Java 8 Lambda新語法,簡化程式,增強效能文章中看到作者用淺顯易懂的方式和程式碼來做示範,真心覺得容易上手,非常推薦這篇文章;經過努力不懈的學習,對於Java 8
有了初步的一個認識。下一篇將會針對這些特性一個一個的實作及介紹。
註:以上參考了
Java 8 Lambda新語法,簡化程式,增強效能
Java 8 新特性
Java 8的新特性—终极版
现代化 Java - Java8 指南