1. 介紹
老實說:為了一兩行程式碼就去寫匿名類別——就像租一台巨型卡車,只為了把一家麵包店的一個小麵包送到商店。
例如,若你想依長度排序字串清單,在 Java 8 之前必須這樣寫:
List<String> list = Arrays.asList("蘋果", "香蕉", "奇異果");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
看似簡單的需求,卻寫了一半螢幕的程式碼。尤其這類操作很多時,程式碼會變得「雜訊很多」,反而模糊了問題的重點。工程師們又哭又苦,然後就發明了 lambda 運算式。我們之前已經學過一些,現在要來複習並加深理解。
什麼是 Lambda 運算式
Lambda 運算式是一種精簡表達函式式介面實作的方式,也就是只有單一抽象方法的介面(例如 Comparator、Runnable、Consumer 等)。
簡單來說,lambda 運算式讓你能在需要的地方「就地」寫出一個函式,而不必另外宣告類別或方法。
通用語法:
(parametry) -> { telo }
範例:
- (a, b) -> a + b — 將兩個數字相加的函式
- x -> x * x — 將數字平方的函式
- () -> System.out.println("Hello!") — 無參數的函式
與函式式介面的關聯:
Lambda 運算式總是可以指派給函式式介面型別的變數,或作為該介面型別的參數傳入方法中。
2. Lambda 運算式的語法
無參數
Runnable r = () -> System.out.println("你好,世界!");
r.run(); // 會輸出:你好,世界!
一個參數
若僅一個參數,可以省略括號:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java 很酷!");
多個參數
括號是必須的:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
主體只有一個運算式
如果主體只有單一運算式,不需要大括號與 return:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
主體是區塊
若需要多個敘述,使用大括號,且有回傳值時要寫 return:
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
參數型別
多數情況可不必標註型別——編譯器會從上下文推斷。但若想,也可以明確標註:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
無回傳值的 lambda
如果介面回傳 void,直接寫指令即可:
list.forEach(s -> System.out.println("元素: " + s));
表格:Lambda 運算式語法變體
| 要做什麼 | 範例 | 說明 |
|---|---|---|
| 無參數 | |
例如用於 Runnable |
| 一個參數 | |
可省略括號 |
| 多個參數 | |
必須加上括號 |
| 單一運算式 | |
不需要 return 與大括號 |
| 程式碼區塊 | |
有回傳值時需使用 return |
3. 應用:什麼時候以及如何使用 lambda
Lambda 運算式最常用在需要把「行為」——也就是函式——當作引數傳遞的情境。這對集合、串流(Stream API)、事件等帶來了革命性的改變。
清單排序
在 Java 8 之前:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
使用 Lambda 運算式:
list.sort((a, b) -> a.length() - b.length());
建立執行緒(Thread)
Thread t = new Thread(() -> System.out.println("執行緒已啟動!"));
t.start();
處理集合
list.forEach(s -> System.out.println(s.toUpperCase()));
篩選清單
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
範例:擴充我們的教學應用程式
假設我們有一個使用者清單:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
輸出所有名字長度大於 4 的使用者:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("使用者: " + name));
4. Lambda 中變數的作用域
Lambda 運算式可以使用外層方法中的變數,但有幾個重點!
變數與 lambda
在 Java 中,lambda 只能「捕捉」那些在初始化之後不再改變的變數。若變數宣告為 final——就很直覺。即使沒有寫 final,編譯器也會檢查它的值是否被變更;如果沒有,就會把它視為「彷彿是 final」,並允許在 lambda 中使用。
範例:
int minLength = 4; // 該值不會在其他地方被修改
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
這裡可行,因為 minLength 始終保持相同的數值。
但如果在 lambda 使用之後你試圖重新指派 minLength,就會得到編譯錯誤:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // 錯誤!Lambda 已「固定」了該值
本質規則很簡單:進入 lambda 的變數必須是不可變的。
與匿名類別的差異
在匿名類別與 lambda 中,外層方法的變數規則相同:只能是 final/有效 final。
但是!
在 lambda 中,this 參照的是外部物件(當前類別的實例),而在匿名類別中,則是匿名類別本身。如果你在 lambda 內部要存取當前類別的欄位或方法,這點相當重要。
範例:
public class Example {
String name = "外部類別";
void demo() {
Runnable r1 = new Runnable() {
String name = "匿名類別";
@Override
public void run() {
System.out.println(this.name); // "匿名類別"
}
};
Runnable r2 = () -> System.out.println(this.name); // "外部類別"
r1.run();
r2.run();
}
}
5. 使用 lambda 運算式的常見錯誤
錯誤一:使用非 final/非有效 final 的變數。 如果你在把變數用於 lambda 之後又想改變它,編譯器會立刻阻止你。這是為了安全性:否則就無法確定該使用哪個值。
錯誤二:混淆 this。 在 lambda 中,this 指向外部類別,而在匿名類別中則指向匿名類別本身。如果你想從 lambda 呼叫外部類別的方法,一切正常;但若在匿名類別中這麼做,就不會按你預期(如果你假定能取得外部類別的脈絡)。
錯誤三:沒有上下文的 lambda。 Lambda 不能單獨存在——你需要把它指派給函式式介面型別的變數,或傳遞到期望該介面的地方。嘗試在上下文之外直接寫 x -> x + 1 會導致錯誤。
錯誤四:Lambda 過於複雜。 如果 lambda 超過 3–5 行,就會難以閱讀。這種情況下,最好把邏輯抽出到獨立方法。
GO TO FULL VERSION