CodeGym /課程 /JAVA 25 SELF /Lambda 運算式:語法與作用域

Lambda 運算式:語法與作用域

JAVA 25 SELF
等級 48 , 課堂 0
開放

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 運算式是一種精簡表達函式式介面實作的方式,也就是只有單一抽象方法的介面(例如 ComparatorRunnableConsumer 等)。

簡單來說,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 運算式語法變體

要做什麼 範例 說明
無參數
() -> System.out.println("Hi")
例如用於 Runnable
一個參數
x -> x * x
可省略括號
多個參數
(a, b) -> a + b
必須加上括號
單一運算式
x -> x + 1
不需要 return 與大括號
程式碼區塊
x -> { int y = x + 1; return y * 2; }
有回傳值時需使用 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 行,就會難以閱讀。這種情況下,最好把邏輯抽出到獨立方法。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION