Like Share Discussion Bookmark Smile

J.J. Huang   2020-03-31   Java   瀏覽次數:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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之後加上大括號「{}」,就判定為重新修改)。這樣說可能還是很模糊,如果對照以下非匿名類別的撰寫方式,就可以清楚匿名和非匿名的差異了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.classA$B.class兩個檔案。

1
2
3
4
5
6
class A {

class B {

}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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方法,或是AWTActionListener只擁有一個actionPerformed方法。這類介面,都可以直接使用Lambda,將它們擁有的那單個方法快速實作出來。在Java 8之前,我們將這類的介面稱為Single Abstract Method type(SAM);在Java 8之後,因為這類的介面變得十分重要,所以將其另稱為Functional Interface

Lambda 語法結構

1
input -> body

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

input

  • 不輸入
1
()
  • 單個輸入
1
x
  • 多個輸入(不省略型態)
1
(int x,int y)
  • 多個輸入(省略型態)
1
(x,y)

body

  • 什麼都不做
1
{}
  • 單行不回傳值
1
System.out.println("NO");
  • 多行不回傳值
1
2
3
4
{
System.out.println("NO");
System.out.println("NO2");
}
  • 單行回傳值
1
x+y
  • 多行回傳值
1
2
3
4
5
{
x++;
y-=x;
return x+y;
}

另外還有一種特殊語法結構,可以省略掉input->,因為會使程式碼較不易閱讀,不建議使用。

Lambda 實際應用與效能比較

取代Functional Interface產出的匿名類別

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

1
2
3
4
5
Runnable runnbale = new Runnable() {
public void run() {
System.out.println("run me!");
}
};

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

1
Runnable runnbale = () -> System.out.println("run me!");

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

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

1
javap -c Class路徑

舉例:

1
javap -c com.morose.A

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

不使用Lambda

1
2
3
0: new #2
3: dup
4: invokespecial #3

使用Lambda

1
0: invokedynamic #2, 0

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

註:如果想了解什麼是bytecode,可以參考此篇文章機器碼(machinecode)和字節碼(bytecode)是什麼?

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

1
2
3
4
5
6
7
8
9
10
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出來的結果卻不一樣。而且可以知道使用Lambdar1this所指的物件就是此行Lambda語法所在的物件,並不會像沒使用Lambdar2一樣變成一個匿名類別的物件。

Lambda 與 Collection 的關係

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

走訪

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

1
2
3
for (String s : list) {
System.out.print(s);
}

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

1
2
3
4
Set<String> keySet = map.keySet();
for (String s : keySet) {
System.out.print(s + ":" + map.get(s));
}

若改以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
2
3
4
5
6
7
8
9
10
11
12
// 原寫法
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
for (String s : list) {
if( Integer.valueOf(s) < 3){
System.out.print(s);
}
}
1
2
3
4
5
6
7
8
// 使用stream
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原寫法
List<String> list = new ArrayList<String>();
List<String> list2 = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
for (String s : list) {
if(Integer.valueOf(s) < 3){
list2.add(s);
}
}
list2.add("7");
for (String s : list2) {
System.out.print(s);
}
1
2
3
4
5
6
7
8
9
10
// 使用stream
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 原寫法
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");

int i = 0;
for (String s : list) {
i += Integer.valueOf(s);
}
System.out.println(i);

int sum = 0;
int size = 0;
for (String s : list) {
int val = Integer.valueOf(s);
if( val < 3){
sum += val;
size++;
}
}
System.out.println((double)sum / size);
1
2
3
4
5
6
7
8
9
// 使用stream
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
2
15
1.5

Lambda 特殊精簡語法結構

注:文章作者有提到這樣的結構雖然可以使得程式變得非常精簡,卻也不易閱讀。

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

1
方法名稱

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
}
}

以上輸出的結果為:

1
2
3
嗨嗨
哈囉
6.256.25

這種撰寫方式,實際上常與forEach搭配,如下:

1
2
3
4
5
6
7
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新語法,簡化程式,增強效能文章中看到作者用淺顯易懂的方式和程式碼來做示範,真心覺得容易上手,非常推薦這篇文章;經過努力不懈的學習,對於Java 8有了初步的一個認識。下一篇將會針對這些特性一個一個的實作及介紹。


註:以上參考了
Java 8 Lambda新語法,簡化程式,增強效能
Java 8 新特性
Java 8的新特性—终极版
现代化 Java - Java8 指南