CodeGym /課程 /JAVA 25 SELF /Java 中的閉包 (closures):特性

Java 中的閉包 (closures):特性

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

1. 認識閉包

閉包 — 是一種函式(或函式物件),它不僅使用自己的參數,還會「記住」建立它時所處外部環境中的變數。更直白地說,如果方法中的 Lambda 運算式或匿名類別使用了該方法的區域變數 — 它就成為閉包。

簡單範例

public class ClosureDemo {
    public static void main(String[] args) {
        String greeting = "你好, ";
        Runnable sayHello = () -> System.out.println(greeting + "世界!");
        sayHello.run(); // 會輸出: 你好, 世界!
    }
}

這裡 Lambda「捕捉」了外部方法中的變數 greeting,並在自身內部使用。這就是閉包!

2. 有效 final 變數:是什麼,為什麼需要?

在 Java 中,Lambda 運算式(和匿名類別)只能使用來自外部方法、且宣告為 final 或在初始化後未再變更的變數。這類變數稱為有效 final

為什麼?

這項限制與以下事實有關:Lambda 運算式可能在建立它的方法結束後才被呼叫。如果變數還能變動,就會產生混淆:應該使用變數的哪個版本?為了避免驚喜,Java 要求該變數是不可變的(或至少表面上不可變)。

範例:正確的用法

public static void main(String[] args) {
    int number = 42; // number — 有效 final
    Runnable r = () -> System.out.println(number);
    r.run(); // 42
}

範例:嘗試在使用後修改變數

public static void main(String[] args) {
    int number = 42;
    Runnable r = () -> System.out.println(number);
    number++; // 錯誤: 變數 number 必須是 final 或有效 final
    r.run();
}

編譯器會拋出錯誤:Variable used in lambda expression should be final or effectively final

有效 final 是…… 指被指派恰好一次,之後就不再變更的變數。不一定要寫上 final,編譯器會自己判斷。

3. Lambda 運算式如何捕捉變數?

當你撰寫使用外部變數的 Lambda 時,Java 會將該變數與 Lambda 一起「打包」。即使建立 Lambda 的方法已經結束,該變數也不會消失——它會隨著閉包一起存活。

示意:Lambda「記住」了變數

public static Runnable createGreeter(String name) {
    // name — 方法參數,會被 Lambda 捕捉
    return () -> System.out.println("你好, " + name + "!");
}

public static void main(String[] args) {
    Runnable greeter = createGreeter("Vasya");
    greeter.run(); // 你好, Vasya!
}

此處變數 namemain 方法的堆疊中已不存在,但 greeter 仍然「記得」它的值。

內部是如何實作的?

Java 編譯器會建立一個特殊的輔助物件(也稱為「capture/display class」),用來保存所有被捕捉的變數。Lambda 運算式會成為一個物件,並持有指向這個「容器」的參考。

4. 閉包範例:回傳使用變數的函式

我們寫一個函式,回傳一個使用其語境中變數的 Lambda:

import java.util.function.IntSupplier;

public class ClosureFactory {
    public static IntSupplier makeAdder(int x) {
        // x — 由 Lambda 捕捉
        return () -> x + 10;
    }

    public static void main(String[] args) {
        IntSupplier adder = makeAdder(5);
        System.out.println(adder.getAsInt()); // 15
    }
}

這裡變數 x 已經從方法堆疊中「離開」,但 Lambda 仍然可以使用它。

5. 為什麼不能修改被捕捉的變數?

public static void main(String[] args) {
    int base = 100;
    Runnable printer = () -> System.out.println(base);
    base = 200; // 錯誤!
    printer.run();
}

編譯器不允許這麼做。若能修改 base,就會不清楚 Lambda 應該使用舊值還是新值。因此 Java 禁止修改被 Lambda 捕捉的區域變數。

在 Lambda 中可以用什麼?

  • 初始化後不再變更的區域變數(有效 final)。
  • 類別欄位(包含 static 與非 static)—— 可以變更,但這是另一種機制(存取物件狀態),而非捕捉區域變數。

6. 與匿名類別的比較

在 Lambda 出現之前,Java 可以透過匿名類別來實作閉包:

public static void main(String[] args) {
    String word = "Java";
    Runnable r = new Runnable() {
        public void run() {
            System.out.println(word);
        }
    };
    r.run(); // Java
}

規則相同:變數 word 必須是 final 或有效 final。

差異:this 的作用域

  • 在匿名類別中,this 指向匿名類別的實例。
  • 在 Lambda 運算式中,this 指向外部物件(例如目前的類別實例)。

7. 閉包與類別欄位

若 Lambda 使用的是類別欄位,嚴格來說這不是「捕捉區域變數」—— 欄位總是可用,且可以被變更。

public class Counter {
    private int count = 0;

    public Runnable makeCounter() {
        return () -> {
            count++;
            System.out.println("計數器: " + count);
        };
    }

    public static void main(String[] args) {
        Counter c = new Counter();
        Runnable r = c.makeCounter();
        r.run(); // 計數器: 1
        r.run(); // 計數器: 2
    }
}

8. 常見錯誤與 Java 中閉包的特性

錯誤 №1:嘗試修改被 Lambda 捕捉的變數。 最常見的錯誤,是在變數已被 Lambda 使用後仍嘗試修改該區域變數。編譯器會提示:Variable used in lambda expression should be final or effectively final

錯誤 №2:以為變數被「凍結」。 在 Java 中,對於類別欄位而言,使用的是原始欄位的參考而非值的拷貝。如果類別欄位改變,Lambda 會看到新值。但在 Lambda 中使用的區域變數則必須是有效 final。

錯誤 №3:以為 Lambda 會為 this 建立新的作用域。 在 Lambda 中,this 是外部物件(容器類別)。在匿名類別中,this 則是匿名類別本身。

錯誤 №4:使用可變的物件。 如果你捕捉的是指向可變物件(例如 List)的參考,即使變數本身是有效 final,你仍然可以在 Lambda 中改變其內容:

public static void main(String[] args) {
    java.util.List<String> list = new java.util.ArrayList<>();
    Runnable r = () -> list.add("Hello");
    r.run();
    System.out.println(list); // [Hello]
}

這裡變數 list 本身沒有變(我們沒有做 list = ...),但其中的物件內容被修改了。

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