CodeGym /Các khóa học /JAVA 25 SELF /Closures trong Java: các đặc điểm

Closures trong Java: các đặc điểm

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Làm quen với closures

Closure — là một hàm (hoặc đối tượng-hàm) không chỉ dùng các tham số của chính nó mà còn “ghi nhớ” các biến từ ngữ cảnh bao quanh nơi nó được tạo ra. Nói ngắn gọn, nếu một biểu thức lambda hoặc một lớp ẩn danh bên trong một phương thức sử dụng các biến của phương thức đó — thì nó trở thành một closure.

Ví dụ đơn giản

public class ClosureDemo {
    public static void main(String[] args) {
        String greeting = "Xin chào, ";
        Runnable sayHello = () -> System.out.println(greeting + "thế giới!");
        sayHello.run(); // Sẽ in: Xin chào, thế giới!
    }
}

Ở đây lambda đã “bắt” biến greeting từ phương thức bên ngoài và sử dụng nó bên trong chính nó. Đó chính là một closure!

2. Biến final hiệu quả: là gì và để làm gì?

Trong Java, các biểu thức lambda (và lớp ẩn danh) chỉ có thể sử dụng những biến từ phương thức bên ngoài nếu các biến đó được khai báo là final hoặc không bị thay đổi sau khi khởi tạo. Những biến như vậy được gọi là final hiệu quả.

Tại sao lại như vậy?

Giới hạn này liên quan đến việc biểu thức lambda có thể được gọi sau khi phương thức nơi nó được tạo ra đã kết thúc. Nếu biến có thể thay đổi, sẽ nảy sinh rắc rối: dùng phiên bản nào của biến? Để tránh bất ngờ, Java yêu cầu biến phải bất biến (hoặc ít nhất trông như bất biến).

Ví dụ: sử dụng đúng

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

Ví dụ: cố gắng thay đổi biến sau khi đã dùng

public static void main(String[] args) {
    int number = 42;
    Runnable r = () -> System.out.println(number);
    number++; // LỖI: biến number phải là final hoặc final hiệu quả
    r.run();
}

Trình biên dịch sẽ báo lỗi: Variable used in lambda expression should be final or effectively final.

Final hiệu quả là... biến được gán đúng một lần và không thay đổi về sau. Không nhất thiết phải viết final, trình biên dịch sẽ tự nhận ra.

3. Lambda bắt biến như thế nào?

Khi bạn viết một lambda sử dụng biến bên ngoài, Java sẽ “đóng gói” biến đó cùng với lambda. Ngay cả khi phương thức nơi lambda được tạo ra đã kết thúc, biến không biến mất — nó sống bên trong closure.

Minh họa: lambda “ghi nhớ” biến

public static Runnable createGreeter(String name) {
    // name — tham số của phương thức, nó sẽ được lambda bắt
    return () -> System.out.println("Xin chào, " + name + "!");
}

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

Ở đây biến name không còn tồn tại trong stack của phương thức main, nhưng greeter vẫn “nhớ” giá trị của nó.

Bên trong được hiện thực thế nào?

Trình biên dịch Java tạo ra một đối tượng trợ giúp đặc biệt (còn gọi là “capture/display class”) để lưu tất cả các biến đã bắt. Biểu thức lambda trở thành một đối tượng có tham chiếu tới “container” này chứa các biến.

4. Ví dụ closure: trả về hàm sử dụng biến

Hãy viết một hàm trả về lambda sử dụng biến từ ngữ cảnh của nó:

import java.util.function.IntSupplier;

public class ClosureFactory {
    public static IntSupplier makeAdder(int x) {
        // x — được lambda bắt
        return () -> x + 10;
    }

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

Ở đây biến x đã “rời” khỏi stack của phương thức, nhưng lambda vẫn có thể sử dụng nó.

5. Vì sao không thể thay đổi biến đã bắt?

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

Trình biên dịch sẽ không cho phép làm vậy. Nếu chúng ta có thể thay đổi base, sẽ không rõ lambda nên dùng phiên bản nào của biến: cũ hay mới? Vì thế Java cấm thay đổi các biến cục bộ đã được lambda bắt.

Có thể dùng gì trong lambda?

  • Biến cục bộ không thay đổi sau khi khởi tạo (final hiệu quả).
  • Các trường của lớp (cả static lẫn không static) — có thể thay đổi chúng, nhưng đó là cơ chế khác (truy cập trạng thái của đối tượng), không phải là bắt biến cục bộ.

6. So sánh với lớp ẩn danh

Trước khi có biểu thức lambda trong Java, có thể tạo closure bằng các lớp ẩn danh:

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

Quy tắc vẫn vậy: biến word phải là final hoặc final hiệu quả.

Khác biệt: phạm vi của this

  • Trong lớp ẩn danh, this tham chiếu đến chính thể hiện của lớp ẩn danh.
  • Trong biểu thức lambda, this tham chiếu đến đối tượng bên ngoài (ví dụ, thể hiện hiện tại của lớp).

7. Closure và các trường của lớp

Nếu lambda sử dụng một trường của lớp, thì đó không phải là “bắt biến cục bộ” theo nghĩa chặt chẽ — trường luôn khả dụng và có thể thay đổi.

public class Counter {
    private int count = 0;

    public Runnable makeCounter() {
        return () -> {
            count++;
            System.out.println("Bộ đếm: " + count);
        };
    }

    public static void main(String[] args) {
        Counter c = new Counter();
        Runnable r = c.makeCounter();
        r.run(); // Bộ đếm: 1
        r.run(); // Bộ đếm: 2
    }
}

8. Những lỗi điển hình và đặc thù của closures trong Java

Lỗi №1: cố gắng thay đổi biến đã được lambda bắt. Lỗi phổ biến nhất là cố gắng thay đổi biến cục bộ sau khi nó đã được dùng trong lambda. Trình biên dịch sẽ báo: Variable used in lambda expression should be final or effectively final.

Lỗi №2: kỳ vọng biến bị “đóng băng”. Trong Java, biến đã bắt không phải là bản sao giá trị mà là tham chiếu đến bản gốc khi nói về trường của lớp. Nếu trường của lớp thay đổi, lambda sẽ thấy giá trị mới. Nhưng các biến cục bộ trong lambda bắt buộc chỉ được là final hiệu quả.

Lỗi №3: kỳ vọng lambda tạo phạm vi mới cho this. Trong lambda, this — là đối tượng bên ngoài (lớp bao chứa). Trong lớp ẩn danh, this — chính là lớp ẩn danh.

Lỗi №4: sử dụng các đối tượng có thể thay đổi (mutable). Nếu bạn bắt một tham chiếu đến đối tượng có thể thay đổi (ví dụ, một danh sách), bạn có thể thay đổi nội dung của nó bên trong lambda, ngay cả khi bản thân biến là final hiệu quả:

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]
}

Ở đây biến list không thay đổi (chúng ta không làm list = ...), nhưng chính đối tượng bên trong nó thì thay đổi.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION