CodeGym /課程 /JAVA 25 SELF /Lambda 表達式入門

Lambda 表達式入門

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

1. 認識

簡單說:Lambda 表達式是一種在需要時快速建立函式介面實作的方式,不必宣告額外的類別或匿名類別。它就像沒有名字的小型方法,可以當作參數傳遞或存到變數中。

Java 曾長期是一種相當「傳統」的 OOP 語言。若你想傳遞一段行為,例如按下按鈕時要做什麼,就得寫匿名類別:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("按鈕已按下!");
    }
});

自 Java 8 起出現了 Lambda 魔法:

button.setOnClickListener(() -> System.out.println("按鈕已按下!"));

其中 () -> System.out.println("按鈕已按下!") 就是 Lambda 表達式。

形式化地說:Lambda 表達式是對函式介面唯一抽象方法實作的精簡寫法。

為什麼重要?

  • 能把行為當成值來傳遞(例如:對清單中的每個元素要做什麼)。
  • 撰寫更精簡、可讀性更高的程式碼。
  • 使用現代 Java API:Stream API、事件處理、非同步任務等。

2. Lambda 表達式語法

一般型式

(parametry) -> vyrazhenie
// 或者
(parametry) -> { blok koda }

不同介面的範例

1. 無參數(例如 Runnable):

Runnable r = () -> System.out.println("哈囉,Lambda!");
r.run();

2. 一個參數(例如 Consumer):

Consumer<String> printer = s -> System.out.println("你傳入了:" + s);
printer.accept("Java");

如果只有一個參數,且其型別可被推斷,參數的小括號可以省略。
String 是變數(s)的型別,我們把它傳進去。

3. 多個參數(例如 Comparator):

Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5

Integer 是變數(ab)的型別。

4. 多行主體(需要大括號,且若有明確回傳值需寫 return):

Function<Integer, Integer> square = x -> {
    int result = x * x;
    return result;
};
System.out.println(square.apply(6)); // 36

第一個 Integer 是回傳型別,第二個 Integer 是參數型別。

省略與精簡

  • 若主體只有一個運算式,可省略大括號與 return
  • 若無參數,寫空的小括號:() -> ...
  • 若只有一個參數,可省略小括號:x -> ...
  • 若超過一個參數,需要小括號:(a, b) -> ...

總結表

看看同一個想法用 Lambda 與匿名類別如何表示:

介面 Lambda 表達式(範例) 匿名類別的等價寫法
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

自定義介面範例

假設我們有一個函式介面:

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}

以前會這樣實作:

Operation sum = new Operation() {
    @Override
    public void apply(int a, int b) {
        return a + b;
    }
};

現在——更現代:

Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8

標準介面範例

Runnable:

Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();

Comparator:

List<String> list = Arrays.asList("蘋果", "香蕉", "奇異果");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);

Function:

Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123

3. 作用域與變數捕捉

外部脈絡中的變數(effectively final)

Lambda 表達式可以使用周圍方法中的變數。但有條件:那些變數必須是effectively final——也就是要麼明確宣告為 final,要麼在初始化後不再被修改。

範例:

String prefix = "結果:";
Function<Integer, String> f = x -> prefix + (x * 2);
// 這裡的 prefix 被「凍結」——之後就不能再修改
System.out.println(f.apply(5)); // 結果:10

如果嘗試在它被 Lambda 使用之後再修改 prefix,編譯器會報錯。

為什麼?
Lambda 表達式可能在宣告該變數的方法結束後才被呼叫。為避免各種「魔幻」的錯誤,Java 只允許使用不可變的變數。

與匿名類別的差異

在匿名類別中也有相同規則:外部方法的變數必須是 final/effectively final。但在作用域上有細節差異:在匿名類別內,this 參照的是匿名類別本身;而在 Lambda 中,this 參照的是外部類別。

public class Demo {
    public void test() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // 輸出:Demo$1(匿名類別)
            }
        };

        Runnable r2 = () -> System.out.println(this); // 輸出:Demo(外部類別)

        r1.run();
        r2.run();
    }
}

Lambda 與類別欄位

Lambda 表達式可以不受限制地存取外部類別的欄位:

public class Counter {
    private int base = 10;

    public void printSum(int x) {
        Function<Integer, Integer> sum = y -> base + y + x;
        System.out.println(sum.apply(5));
    }
}

這裡的 base——可以修改(它是類別欄位)。
x——必須是 effectively final。

4. 實作:寫幾個 Lambda 表達式

範例:篩選數字清單

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);

nums.stream()
    .filter(n -> n % 2 == 0)
    .forEach(n -> System.out.println("偶數:" + n));

更多關於 Stream API 的內容你會在第 30 級學到 :P

範例:字串轉換函式

Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA

範例:自訂函式介面

@FunctionalInterface
interface StringTransformer {
    String transform(String s);
}

StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("你好")); // 你好!

範例:使用外部脈絡中的變數

int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// 之後不能修改 factor!

5. 使用 Lambda 表達式的常見錯誤

錯誤 №1:試圖修改被 Lambda 捕捉的變數。
這樣的程式不會通過編譯——該變數必須是 effectively final:

int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // 錯誤:sum 不是 final!

如果需要累加值——請使用陣列或包裝物件。

錯誤 №2:混淆 this 的作用域。
在 Lambda 表達式內,this 指向外部類別,而不是指向 Lambda(與匿名類別不同)。

錯誤 №3:在多行 Lambda 主體中忘了大括號與 return
如果 Lambda 主體不止一個運算式,就需要大括號與 return

Function<Integer, Integer> square = x -> {
    int y = x * x;
    return y;
};

錯誤 №4:錯誤地指定 Lambda 表達式的型別。
Lambda 表達式一定是實作某個函式介面。不能只寫:

var f = x -> x + 1; // 錯誤!無法得知介面型別。

必須明確指定型別:

Function<Integer, Integer> f = x -> x + 1;
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION