Menu

Biểu thức lambda trong Java: viết code hàm ngắn gọn

Biểu thức lambda trong Java là gì, cú pháp mũi tên, cách lambda triển khai functional interface, method reference và cách lambda dùng biến bên ngoài.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Hành vi có thể truyền đi

Biểu thức lambda (lambda expression) là một khối code gọn mà bạn có thể truyền cho phương thức, lưu vào biến hoặc trả về, giống như với một con số hay một chuỗi. Trước khi có lambda, muốn truyền hành vi thì phải viết cả một lớp (hoặc một lớp ẩn danh dài dòng) chỉ để bọc một phương thức. Lambda rút gọn tất cả về phần cốt lõi: tham số và phần thân.

Lambda luôn triển khai một functional interface, tức interface có đúng một phương thức trừu tượng (bạn đã gặp ở cuối trang interface). Trình biên dịch suy ra bạn đang muốn interface nào từ ngữ cảnh và ghép lambda với phương thức duy nhất đó.

x -> x * 2 là toàn bộ phần cài đặt của apply. Không new, không thân lớp, không tên phương thức: interface đã lo hết những thứ đó.

Cú pháp mũi tên

Lambda nào cũng có dạng parameters -> body. Các phần có thể co giãn tùy theo nhu cầu:

() -> 42                       // no parameters
x -> x + 1                     // one parameter, parentheses optional
(x, y) -> x + y                // two or more parameters need parentheses
(int x, int y) -> x + y        // types are optional - usually inferred
x -> {                          // a block body needs braces and return
    int doubled = x * 2;
    return doubled + 1;
}

Phần thân chỉ gồm một biểu thức (x -> x + 1) sẽ ngầm trả về giá trị của nó, không cần từ khóa return. Ngay khi dùng dấu ngoặc nhọn, bạn đang viết một khối lệnh bình thường và phải return tường minh nếu phương thức có trả về giá trị. Lỗi hay gặp là trộn hai cách: x -> { x + 1 } không biên dịch được, vì một khối lệnh cần một câu lệnh (return x + 1;).

Lambda thay thế lớp ẩn danh

Cách dễ thấy nhất lợi ích của lambda là so sánh trước và sau. Sắp xếp với một Comparator tùy chỉnh trước đây trông như thế này:

// before - anonymous class
names.sort(new Comparator<String>() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

Viết lại bằng lambda chỉ còn một dòng dễ đọc:

Comparator là một functional interface (phương thức trừu tượng duy nhất của nó là compare), nên lambda khớp vào ngay. Mọi thủ tục rườm rà như new, lớp, chữ ký phương thức đều biến mất, chỉ còn lại logic so sánh.

Bộ công cụ java.util.function

Bạn hiếm khi phải tự khai báo functional interface. Package java.util.function có sẵn các dạng thông dụng, và gần như mọi API trong thư viện Java đều nhận chúng:

  • Function<T, R>: nhận một T, trả về một R (apply)
  • Predicate<T>: nhận một T, trả về một boolean (test)
  • Supplier<T>: không nhận gì, trả về một T (get)
  • Consumer<T>: nhận một T, không trả về gì (accept)

Đây là các interface generic: Function<String, Integer> dùng lại generics mà bạn đã thấy ở trang trước để giữ an toàn kiểu. Hãy chọn interface có dạng khớp với thứ code của bạn cần nhận vào và tạo ra.

Method reference

Khi một lambda không làm gì ngoài gọi một phương thức có sẵn, bạn có thể thay nó bằng một method reference (tham chiếu phương thức) dùng ::. Giá trị vẫn như cũ, chỉ là cách viết trực tiếp hơn:

Method reference có vài dạng: String::toUpperCase (phương thức instance được gọi trên từng đối số), Math::max (phương thức static), System.out::println (phương thức của một đối tượng cụ thể) và ArrayList::new (hàm khởi tạo). Chỉ dùng khi nó đọc rõ ràng; nếu phải nghĩ xem dạng nào áp dụng, một lambda bình thường là đủ.

Bắt giữ biến

Lambda có thể dùng biến cục bộ của phương thức bao quanh, nhưng chỉ khi biến đó là final hoặc effectively final, tức được gán một lần và không bao giờ đổi. Java bắt giữ giá trị ngay lúc lambda được tạo, nên một biến có thể đổi về sau sẽ gây mơ hồ.

Nếu bạn gán lại factor ở bất kỳ đâu, trình biên dịch sẽ từ chối lambda với thông báo "variable used in lambda expression should be final or effectively final." Khi thật sự cần trạng thái dùng chung có thể thay đổi, hãy bắt giữ một đối tượng: một trường, một phần tử mảng hoặc một AtomicInteger, vì tham chiếu vẫn cố định dù nội dung bên trong thay đổi. Lưu ý rằng lambda, khác với lớp ẩn danh, không tạo phạm vi riêng: this bên trong lambda trỏ tới instance bao quanh, không phải chính lambda.

Tiếp theo: Stream

Lambda là viên gạch nền, không phải đích đến. Giá trị thật sự của nó đến cùng Streams API, nơi bạn nối các thao tác như filter, map và reduce, mỗi thao tác nhận một lambda, để diễn đạt việc biến đổi dữ liệu thành một pipeline dễ đọc thay vì một mớ vòng lặp rối rắm. Đó là nội dung của trang tiếp theo.

Câu hỏi thường gặp

Biểu thức lambda trong Java là gì?

Lambda là cách viết ngắn gọn một instance của functional interface, tức interface chỉ có đúng một phương thức trừu tượng. Thay vì viết cả một lớp ẩn danh, bạn chỉ cần viết parameters -> body. Trình biên dịch ghép lambda với phương thức duy nhất của interface, nên Runnable r = () -> System.out.println("hi"); đã là một Runnable hoàn chỉnh. Nhờ lambda, bạn truyền hành vi đi khắp nơi như truyền dữ liệu.

Lambda và method reference trong Java khác nhau thế nào?

Cả hai đều tạo ra một instance của functional interface. Lambda có tham số và phần thân rõ ràng (s -> s.toUpperCase()), còn method reference là cách viết tắt cho một lambda chỉ gọi đúng một phương thức có sẵn (String::toUpperCase). Hãy dùng method reference khi lambda không làm gì ngoài chuyển tiếp đối số cho một phương thức có tên: cách viết này ngắn hơn và dễ đọc hơn.

Vì sao biến dùng trong lambda Java phải là final hoặc effectively final?

Lambda có thể dùng biến cục bộ của phương thức bao quanh, nhưng chỉ khi biến đó không bao giờ thay đổi sau khi gán, đó chính là ý nghĩa của "effectively final". Java bắt giữ giá trị chứ không giữ một tham chiếu sống tới biến, nên nếu cho phép gán lại thì kết quả sẽ mơ hồ và không an toàn giữa các luồng. Nếu thật sự cần trạng thái dùng chung có thể thay đổi, hãy dùng một trường, một mảng hoặc lớp bọc AtomicInteger.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu