CodeGym /Các khóa học /JAVA 25 SELF /Giới thiệu về biểu thức lambda

Giới thiệu về biểu thức lambda

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

1. Giới thiệu

Nói ngắn gọn: biểu thức lambda — là cách nhanh để tạo hiện thực của một functional interface “ngay tại chỗ”, không cần khai báo lớp riêng hay lớp ẩn danh. Nó giống như một phương thức nhỏ không có tên, có thể truyền làm đối số hoặc lưu vào biến.

Trong thời gian dài, Java là một ngôn ngữ OOP “cổ điển”. Nếu bạn muốn truyền một phần hành vi, ví dụ làm gì khi nhấn nút, bạn phải viết các lớp ẩn danh:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("Nút đã được nhấn!");
    }
});

Từ Java 8 xuất hiện “phép màu” lambda:

button.setOnClickListener(() -> System.out.println("Nút đã được nhấn!"));

Ở đây () -> System.out.println("Nút đã được nhấn!") — chính là một biểu thức lambda.

Về mặt hình thức: biểu thức lambda là cách viết gọn gàng cho việc hiện thực hóa phương thức trừu tượng duy nhất của một functional interface.

Vì sao điều này quan trọng?

  • Truyền hành vi như một giá trị (ví dụ, làm gì với mỗi phần tử của danh sách).
  • Viết mã ngắn gọn và dễ đọc.
  • Sử dụng các API hiện đại của Java: Stream API, xử lý sự kiện, tác vụ bất đồng bộ và nhiều hơn nữa.

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

Hình thức chung

(parametry) -> vyrazhenie
// hoặc
(parametry) -> { blok koda }

Ví dụ cho các interface khác nhau

1. Không có tham số (ví dụ, Runnable):

Runnable r = () -> System.out.println("Xin chào, lambda!");
r.run();

2. Một tham số (ví dụ, Consumer):

Consumer<String> printer = s -> System.out.println("Bạn đã truyền: " + s);
printer.accept("Java");

Nếu chỉ có một tham số và kiểu có thể suy ra, có thể bỏ ngoặc tròn.
Kiểu String là kiểu của biến (s) mà chúng ta truyền vào.

3. Nhiều tham số (ví dụ, Comparator):

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

Kiểu Integer là kiểu của các biến (a, b) mà chúng ta truyền vào.

4. Thân nhiều dòng (cần ngoặc nhọn và return nếu có trả về tường minh):

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

Integer thứ nhất là kiểu trả về của hàm, Integer thứ hai là kiểu của tham số.

Rút gọn và ngắn gọn

  • Nếu thân chỉ có một biểu thức, có thể bỏ ngoặc nhọn và return.
  • Nếu không có tham số — viết ngoặc tròn rỗng: () -> ...
  • Nếu có một tham số — có thể không cần ngoặc: x -> ...
  • Nếu có nhiều hơn một tham số — cần ngoặc: (a, b) -> ...

Bảng tóm tắt

Hãy xem cùng một ý tưởng được viết bằng lambda và bằng lớp ẩn danh như thế nào:

Interface Biểu thức lambda (ví dụ) Tương đương với lớp ẩn danh
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; } }

Ví dụ với interface tự định nghĩa

Giả sử chúng ta có một functional interface:

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

Trước đây hiện thực như sau:

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

Còn bây giờ — theo cách hiện đại:

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

Ví dụ cho các interface chuẩn

Runnable:

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

Comparator:

List<String> list = Arrays.asList("táo", "chuối", "kiwi");
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. Phạm vi và bắt biến (capture)

Biến từ ngữ cảnh bên ngoài (effectively final)

Biểu thức lambda có thể sử dụng các biến từ phương thức bao quanh. Nhưng có một quy tắc: những biến như vậy phải là effectively final — tức là hoặc được khai báo rõ ràng là final, hoặc đơn giản là không bị thay đổi sau khi khởi tạo.

Ví dụ:

String prefix = "Kết quả: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix ở đây “đóng băng” — sau đó không được phép thay đổi
System.out.println(f.apply(5)); // Kết quả: 10

Nếu cố gắng thay đổi prefix sau khi nó được dùng trong lambda, trình biên dịch sẽ báo lỗi.

Tại sao lại như vậy?
Biểu thức lambda có thể được gọi sau khi thoát khỏi phương thức nơi biến được khai báo. Để tránh lỗi “kỳ quặc”, Java chỉ cho phép sử dụng các biến bất biến.

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

Trong lớp ẩn danh cũng có quy tắc tương tự: các biến từ phương thức bên ngoài phải là final/effectively final. Nhưng có khác biệt về phạm vi: bên trong lớp ẩn danh, this tham chiếu đến chính lớp ẩn danh, còn trong lambda — tham chiếu đến lớp bên ngoài.

public class Demo {
    public void test() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // In ra: Demo$1 (lớp ẩn danh)
            }
        };

        Runnable r2 = () -> System.out.println(this); // In ra: Demo (lớp bên ngoài)

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

Lambda và các trường của lớp

Biểu thức lambda có thể truy cập các trường của lớp bên ngoài mà không bị hạn chế:

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));
    }
}

Ở đây base — có thể thay đổi (đây là trường của lớp).
x — phải là effectively final.

4. Thực hành: viết một vài biểu thức lambda

Ví dụ: lọc danh sách số

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

nums.stream()
    .filter(n -> n % 2 == 0)
    .forEach(n -> System.out.println("Số chẵn: " + n));

Tìm hiểu thêm về Stream API ở cấp 30 :P

Ví dụ: hàm chuyển đổi chuỗi

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

Ví dụ: functional interface của riêng bạn

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

StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Xin chào")); // Xin chào!

Ví dụ: sử dụng biến từ ngữ cảnh bên ngoài

int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// không thể thay đổi factor sau đó!

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

Lỗi số 1: cố gắng thay đổi biến được lambda bắt (capture).
Mã như vậy sẽ không biên dịch — biến phải là effectively final:

int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Lỗi: sum không phải final!

Nếu cần tích lũy giá trị — hãy dùng mảng hoặc một đối tượng bao (wrapper).

Lỗi số 2: nhầm lẫn phạm vi của this.
Bên trong biểu thức lambda, this tham chiếu đến lớp bên ngoài, không phải lambda (khác với lớp ẩn danh).

Lỗi số 3: quên ngoặc nhọn và return trong lambda nhiều dòng.
Nếu thân lambda không phải một biểu thức duy nhất, cần có ngoặc nhọn và return:

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

Lỗi số 4: xác định kiểu của biểu thức lambda không đúng.
Biểu thức lambda luôn hiện thực một functional interface. Không thể chỉ viết:

var f = x -> x + 1; // Lỗi! Không biết là kiểu interface nào.

Cần chỉ rõ kiểu:

Function<Integer, Integer> f = x -> x + 1;
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION