CodeGym /Các khóa học /JAVA 25 SELF /Biểu thức lambda: cú pháp và phạm vi

Biểu thức lambda: cú pháp và phạm vi

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

1. Giới thiệu

Thẳng thắn nhé: viết các lớp ẩn danh chỉ vì một hai dòng code — giống như thuê một chiếc xe tải khổng lồ chỉ để chở một chiếc bánh mì từ tiệm bánh đến cửa hàng.

Ví dụ, nếu bạn muốn sắp xếp danh sách chuỗi theo độ dài, trước Java 8 bạn phải viết như sau:

List<String> list = Arrays.asList("táo", "chuối", "kiwi");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Nhiệm vụ có vẻ đơn giản, nhưng lượng code — chiếm nửa màn hình. Đặc biệt khó chịu khi có nhiều thao tác như vậy: code trở nên “ồn ào”, mất đi trọng tâm của bài toán. Lập trình viên đã khóc than, khổ sở, rồi họ nghĩ ra biểu thức lambda. Chúng ta đã học qua một chút, giờ sẽ ôn lại và đào sâu kiến thức.

Biểu thức lambda là gì

Biểu thức lambda là cách viết gọn để hiện thực một functional interface, tức là interface chỉ có một phương thức trừu tượng (ví dụ, Comparator, Runnable, Consumer và nhiều interface khác).

Nói đơn giản, biểu thức lambda cho phép bạn viết một hàm “ngay tại chỗ”, đúng nơi cần dùng, mà không phải khai báo lớp hoặc phương thức riêng.

Cú pháp chung:

(tham số) -> { thân }

Ví dụ:

  • (a, b) -> a + b — hàm cộng hai số
  • x -> x * x — hàm bình phương một số
  • () -> System.out.println("Hello!") — hàm không có tham số

Mối liên hệ với functional interface:
Biểu thức lambda luôn có thể gán cho biến kiểu functional interface hoặc truyền làm đối số vào phương thức đang kỳ vọng interface đó.

2. Cú pháp của biểu thức lambda

Không có tham số

Runnable r = () -> System.out.println("Xin chào, thế giới!");
r.run(); // In ra: Xin chào, thế giới!

Một tham số
Nếu chỉ có một tham số, có thể bỏ dấu ngoặc tròn:

Consumer<String> print = s -> System.out.println(s);
print.accept("Java thật tuyệt!");

Nhiều tham số
Dấu ngoặc là bắt buộc:

Comparator<String> cmp = (a, b) -> a.length() - b.length();

Thân chỉ có một biểu thức
Nếu thân chỉ gồm một biểu thức, không cần dấu ngoặc nhọn và return:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

Thân là một khối lệnh
Nếu cần nhiều câu lệnh, hãy dùng dấu ngoặc nhọn và return (nếu có giá trị trả về):

Function<Integer, Integer> abs = x -> {
    if (x < 0) {
        return -x;
    }
    return x;
};
System.out.println(abs.apply(-3)); // 3

Kiểu của tham số
Thường thì có thể không chỉ rõ kiểu tham số — trình biên dịch sẽ suy ra từ ngữ cảnh. Nhưng nếu muốn, bạn có thể ghi rõ:

Comparator<String> cmp = (String a, String b) -> a.length() - b.length();

Lambda không có giá trị trả về
Nếu interface trả về void, chỉ cần viết các câu lệnh:

list.forEach(s -> System.out.println("Phần tử: " + s));

Bảng: Các biến thể cú pháp của biểu thức lambda

Làm gì Ví dụ Ghi chú
Không có tham số
() -> System.out.println("Hi")
Ví dụ cho Runnable
Một tham số
x -> x * x
Có thể bỏ ngoặc
Nhiều tham số
(a, b) -> a + b
Bắt buộc có ngoặc
Một biểu thức
x -> x + 1
Không cần return và dấu ngoặc nhọn
Khối mã
x -> { int y = x + 1; return y * 2; }
Có return nếu có kết quả

3. Ứng dụng: dùng biểu thức lambda ở đâu và như thế nào

Biểu thức lambda thường được dùng ở nơi cần truyền “hành vi” — tức một hàm — như một đối số. Điều này đã tạo ra cuộc cách mạng cho collection, stream (Stream API), xử lý sự kiện và còn nhiều lĩnh vực khác.

Sắp xếp danh sách

Trước Java 8:

list.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Với lambda:

list.sort((a, b) -> a.length() - b.length());

Tạo luồng (Thread)

Thread t = new Thread(() -> System.out.println("Luồng đã khởi chạy!"));
t.start();

Xử lý collection

list.forEach(s -> System.out.println(s.toUpperCase()));

Lọc danh sách

List<String> longWords = list.stream()
    .filter(s -> s.length() > 5)
    .collect(Collectors.toList());

Ví dụ: Phát triển ứng dụng học tập của chúng ta

Giả sử chúng ta có danh sách người dùng:

List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

In ra tất cả người dùng có tên dài hơn 4 ký tự:

users.stream()
    .filter(name -> name.length() > 4)
    .forEach(name -> System.out.println("Người dùng: " + name));

4. Phạm vi của biến trong biểu thức lambda

Biểu thức lambda có thể sử dụng các biến từ phương thức bao quanh, nhưng có vài lưu ý!

Biến và lambda

Lambda trong Java chỉ có thể “bắt” những biến mà sau khi khởi tạo không còn bị thay đổi. Nếu biến được khai báo với final — điều đó là hiển nhiên. Nhưng ngay cả khi không ghi final, trình biên dịch vẫn kiểm tra: giá trị có thay đổi hay không. Nếu không, nó coi biến đó như “effectively final” và cho phép dùng trong lambda.

Ví dụ:

int minLength = 4; // giá trị không thay đổi ở đâu cả
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});

Ở đây mọi thứ hoạt động vì minLength vẫn giữ nguyên giá trị.

Nhưng nếu sau khi dùng trong lambda bạn thử gán lại minLength, bạn sẽ nhận lỗi biên dịch:

int minLength = 4;
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});
minLength = 10; // Lỗi! Lambda đã "cố định" giá trị

Tóm lại quy tắc rất đơn giản: biến đã lọt vào lambda thì phải là bất biến.

Khác biệt với lớp ẩn danh

Trong cả lớp ẩn danh và biểu thức lambda, biến từ phương thức bên ngoài hoạt động giống nhau: chỉ final/effectively final.

NHƯNG!
Trong biểu thức lambda, this tham chiếu tới đối tượng bên ngoài (thể hiện hiện tại của lớp), còn trong lớp ẩn danh — tới chính thể hiện lớp ẩn danh. Điều này quan trọng nếu bạn gọi tới trường hoặc phương thức của lớp hiện tại bên trong lambda.

Ví dụ:

public class Example {
    String name = "Lớp bên ngoài";

    void demo() {
        Runnable r1 = new Runnable() {
            String name = "Lớp ẩn danh";
            @Override
            public void run() {
                System.out.println(this.name); // "Lớp ẩn danh"
            }
        };

        Runnable r2 = () -> System.out.println(this.name); // "Lớp bên ngoài"

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

5. Những lỗi thường gặp khi làm việc với biểu thức lambda

Lỗi số 1: Dùng biến không phải final/effectively final. Nếu bạn quyết định thay đổi biến sau khi đã dùng nó trong lambda, trình biên dịch sẽ báo lỗi ngay. Điều này nhằm đảm bảo an toàn: nếu không sẽ không rõ nên dùng giá trị nào của biến.

Lỗi số 2: Nhầm lẫn với this. Trong lambda, this — là lớp bên ngoài, còn trong lớp ẩn danh — là chính lớp ẩn danh. Nếu bạn cố gọi phương thức của lớp bên ngoài từ lambda, mọi thứ sẽ hoạt động; còn từ lớp ẩn danh thì không (nếu bạn trông đợi ngữ cảnh của lớp bên ngoài).

Lỗi số 3: Lambda không có ngữ cảnh. Biểu thức lambda không thể dùng một mình — cần gán cho biến kiểu functional interface hoặc truyền vào nơi interface đó được kỳ vọng. Thử chỉ viết x -> x + 1 bên ngoài ngữ cảnh sẽ gây lỗi.

Lỗi số 4: Lambda quá phức tạp. Nếu biểu thức lambda dài hơn 3–5 dòng, nó sẽ khó đọc. Trong trường hợp đó, tốt hơn là tách logic ra một phương thức riêng.

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