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的人都有用過類似以下的方式去實體化一個物件出來:
class ClassA {
}
public class MainClass {
public static void main(String[] args) {
ClassA a = new ClassA() { //實體化匿名類別
public void function1(int x, int y) {
System.out.println(x + y);
}
public int function2(int x) {
return x + 1;
}
};
}
}
以上是將匿名類別實體化出來的一段程式。為何說程式碼內的ClassA
是匿名的呢?其實匿名類別指的不是ClassA
(ClassA
已有ClassA
這個名稱,自然不會是一個匿名類別),而是在new的同時,被重新修改過的ClassA
(在new statment之後加上大括號{}
,就判定為重新修改)。這樣說可能還是很模糊,如果對照以下非匿名類別的撰寫方式,就可以清楚匿名和非匿名的差異了。
class ClassA {
}
class ClassB extends ClassA { //在另外的地方宣告class,並且繼承或是實作ClassA
public void function1(int x, int y) {
}
public int function2(int x) {
return x + 1;
}
}
public class MainClass {
public static void main(String[] args) {
ClassA a = new ClassB();
}
}
將原先使用的匿名類別,另外宣告成一個ClassB
出來,調用時直接new ClassB
,此處的類別便不是匿名類別,而相比每次只能夠在同一個程式碼位置new出實體的匿名類別,ClassB
則可以在任何Scope所及的地方new出實體。
我們都知道,Java會把每個類別在編譯時期,都創建出相對應的.class
檔案。如果是一般的類別,則會直接以Class的名稱作為.class
檔案的檔名。如果是內部(Inner)的非匿名類別,則會以$
為分隔字元,表示類別在哪層類別之下,作為.class
檔案的檔名。舉例,編譯出以下的程式,會得到A.class
與A$B.class
兩個檔案。
class A {
class B {
}
}
但如果有內部的匿名類別,那.class
會如何產生呢?無論匿名類別是從何種類別重新修改而來,Java都會按照匿名類別是在哪層底下,給予流水的數字編號。舉例,編譯出以下的程式,會得到C.class
、A.class
、A$B.class
、A$1.class
與A$2.class
五個檔案。
class C {
}
class A {
class B {
}
public void f2() {
C c = new C() {
};
}
public void f1() {
B c = new B() {
};
}
}
由此可知,匿名類別實際上也擁有一個.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語法結構
input -> body
其中,input
和body
都各有多種撰寫方式,以下分別舉例。
input
不輸入
()
單個輸入
x
多個輸入(不省略型態)
(int x,int y)
多個輸入(省略型態)
(x,y)
body
什麼都不做
{}
單行不回傳值
System.out.println("NO");
多行不回傳值
{
System.out.println("NO");
System.out.println("NO2");
}
單行回傳值
x+y
多行回傳值
{
x++;
y-=x;
return x+y;
}
以上幾種方式,可以自己練習組合看看。另外還有一種特殊語法結構,可以省略掉input
和->
,因為會使程式碼較不易閱讀,不建議使用,將在文章最後提到。
Lambda實際應用與效能比較
取代Functional Interface產出的匿名類別
在Java中,許多只有一個方法的介面,如果要使用這些介面,往往需要使用到至少4行程式碼才有辦法達成,舉例:
Runnable runnbale = new Runnable() {
public void run() {
System.out.println("run me!");
}
};
這樣的使用方式,往往會讓Java程式拉的很長,變得十分複雜。因此Lambda最重要的功能,就是取代Functional Interface所產出的匿名類別,簡化程式碼,甚至還可以提升效能(稍候提到)。舉例,同樣的Runnable使用方式,可以減化成:
Runnable runnbale = () -> System.out.println("run me!");
使用Lambda來取代以往Functional Interface的使用方式,可以大大的縮短程式碼,在編譯的過程中,也可以避免掉產生新的.class
檔案出來,執行的時候,也不會再重新new出一個物件實體,而是直接將Lambda的body程式碼存放在記憶體,直接以類似call function的方式去執行,大大的提升程式的執行效能。
如果有興趣知道Lambda是如何運作,可以查看編譯出來.class
檔案的bytecode,查看命令如下:
舉例:
比較原先不使用Lambda,與使用Lambda的方式:
不使用Lambda
0: new #2 3: dup 4: invokespecial #3
使用Lambda
0: invokedynamic #2, 0
可以發現使用Lambda就連bytecode,都可以從三行命令,縮減成一行命令。invokedynamic
就是動態調用記憶體內的某段程式碼。
此外,因為少了new
這個指令,使用Lambda實作出來的方法,並不會另外實體化出物件,因此會有以下這個現象發生:
Runnable r1 = () -> System.out.println("r1: " + this.getClass());
Runnable r2 = new Runnable(){
public void run(){
System.out.println("r2: " + this.getClass());
}
};
new Thread(r1).start();
new Thread(r2).start();
雖然兩個執行緒都是呼叫this.getClass()
,但print出來的結果卻不一樣。而且可以知道使用Lambda的r1
,this
所指的物件就是此行Lambda語法所在的物件,並不會像沒使用Lambda的r2
一樣變成一個匿名類別的物件。
Lambda與Collection的關係
Lambda除了用來取代以往使用Functional Interface的方式外,還可以用在Collection的走訪、過濾和一些簡單的運算上。而且同樣地,使用Lambda可以增加不少的效能。
走訪
過去我們使用For each走訪一個List可能要寫成這樣:
for (String s : list) {
System.out.print(s);
}
如果要走訪一個Map更麻煩,可能要寫成這樣:
Set<String> keySet = map.keySet();
for (String s : keySet) {
System.out.print(s + ":" + map.get(s));
}
若改以Lambda來走訪這些Collection,可以簡化成這樣:
list.forEach(s -> System.out.print(s));
map.forEach((k, v) -> System.out.print(k + ":" + v));
程式碼精簡許多,因為少了迴圈,就連bytecode也能將原先十幾行指令省略到剩下三行。
過濾和基本運算
在新的Java 8中,Collection提供了stream()
方法,可以對集合做一些過濾和基本運算,而且這個當然也是有經過效能優化過的。除了stream()
方法外還有parallelStream()
方法,可以讓Collection各別針對它的entry另開出一個Thread,進行stream提供的運算,讓多核心的CPU資源更能有效的被利用。以下將舉幾個過濾和基本運算的例子:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
list.stream().filter(s -> Integer.valueOf(s) < 3).forEach(s -> System.out.print(s));
以上會輸出12
。
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
List<String> list2 = list.stream().filter(s -> Integer.valueOf(s) < 3).collect(Collectors.toList());
list2.add("7");
list2.forEach(s -> System.out.print(s));
以上會輸出127
。
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
System.out.println(list.stream().mapToInt(s->Integer.valueOf(s)).sum());
System.out.println(list.stream().filter(s -> Integer.valueOf(s) < 3).mapToInt(s->Integer.valueOf(s)).average().getAsDouble());
以上會輸出
1.5
Lambda特殊精簡語法結構
除了前面介紹到的input -> body
結構,Lambda在某些特定的場合下,還能夠寫出更短的語法,結構如下:
什麼!?只要寫方法名稱就好了嗎?甚至連參數也不用傳!是的,Lambda允許這種類型的語法存在,但必須要明確指定方法名稱是在哪個類別或是哪個物件之下,而且最後一個.
要改成::
。這樣說好像很模糊,舉幾個例子好了:
interface B {
public void doStringWork(String s);
}
interface C {
public double doComputeWork(float x, float y);
}
public class A {
public A() {
B b = this::printOnce;
b.doStringWork("哈囉");
}
public static void main(String[] args) {
B b = A::printTwice;
b.doStringWork("嗨");
new A();
C c = Math::pow;
b.doStringWork(String.valueOf(c.doComputeWork(2.5f, 2)));
}
public static void printTwice(String s) {
System.out.print(s);
System.out.println(s);
}
public void printOnce(String s) {
System.out.println(s);
}
}
以上輸出結果為:
哈囉
6.256.25
這樣的結構雖然可以使得程式變得非常精簡,卻也不易閱讀。這種撰寫方式,實際上常與forEach搭配,如下:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
list.forEach(System.out::print);
以上程式會輸出12354
。
總結
Java 8依靠Lambda新語法,簡化程式,改善效能,在結構上淺顯易懂、容易使用,實屬不錯的新功能!