Lambda và Lập trình hàm trong Java 8

Biểu thức Lambda

Cú pháp cơ bản của biểu thức Lambda: (parameters) -> expression hoặc (parameters) ->{ statements; }

  • () -> đại diện cho một biểu thức lambda
  • Mã đơn dòng không cần viết return (dù giao diện hàm có trả về giá trị hay không), dấu ngoặc nhọn
  • Mã nhiều dòng phải có dấu ngoặc nhọn, có giá trị trả về thì phải ghi rõ giá trị trả về
  • Mã đơn dòng có tham số có thể không cần viết () như s->System.out.println(s)
  • Tham số (T t) có thể ghi hoặc không ghi kiểu dữ liệu

Ví dụ:

// 1. Không cần tham số, trả về giá trị 5  
() -> 5  
  
// 2. Nhận một tham số (kiểu số), trả về gấp đôi giá trị  
x -> 2 * x  
  
// 3. Nhận 2 tham số (kiểu số), trả về hiệu của chúng  
(x, y) -> x - y  
  
// 4. Nhận 2 số nguyên, trả về tổng của chúng  
(int x, int y) -> x + y  
  
// 5. Nhận một đối tượng string, in ra console, không trả về giá trị  
(String s) -> System.out.print(s)  

Các trường hợp sử dụng chính

  1. Đơn giản hóa mã của lớp ẩn danh
  2. Giảm thiểu việc tạo phương thức không cần thiết
  3. Xử lý sự kiện
  4. Sử dụng trong Stream

Lập trình hàm

Giao diện hàm

1. Khái niệm

Giao diện hàm trong Java là giao diện có duy nhất một phương thức trừu tượng.

Giao diện hàm, tức là giao diện phù hợp cho các tình huống lập trình hàm. Trong Java, lập trình hàm được thể hiện qua Lambda, vì vậy giao diện hàm là giao diện có thể sử dụng Lambda. Chỉ khi đảm bảo giao diện có duy nhất một phương thức trừu tượng, Lambda trong Java mới có thể suy luận chính xác.

Ghi chú: "Cú pháp đường" (syntax sugar) là cú pháp mã dễ sử dụng hơn nhưng nguyên lý không thay đổi. Ví dụ, cú pháp for-each khi duyệt tập hợp thực chất vẫn sử dụng iterator ở tầng cài đặt, đây chính là "cú pháp đường". Về mặt ứng dụng, Lambda trong Java có thể coi là "cú pháp đường" của lớp nội ẩn danh, nhưng về nguyên lý thì khác nhau.

2. Định dạng

Giao diện chỉ có thể có một phương thức trừu tượng

@FunctionalInterface
interface Callback {
    void callback();
}

3. Ghi chú @FunctionalInterface

Khi sử dụng ghi chú này để định nghĩa giao diện, trình biên dịch sẽ kiểm tra bắt buộc giao diện có đúng một phương thức trừu tượng, nếu không sẽ báo lỗi. Lưu ý rằng ngay cả khi không sử dụng ghi chú này, miễn là thỏa mãn định nghĩa giao diện hàm, nó vẫn là giao diện hàm và sử dụng như nhau.

  • Giao diện được chú thích @FunctionalInterface, thỏa mãn ràng buộc của giao diện hàm.
  • Giao diện không được chú thích @FunctionalInterface nhưng thỏa mãn ràng buộc của giao diện hàm.

Ràng buộc của giao diện hàm:

  • Giao diện có duy nhất một phương thức trừu tượng, chỉ có định nghĩa phương thức, không có thân phương thức.
  • Phương thức ghi đè trong Object class không được tính là phương thức của giao diện hàm.
  • Phương thức default trong giao diện không được tính là phương thức của giao diện hàm.
  • Phương thức static trong giao diện không được tính là phương thức của giao diện hàm.

Giao diện hàm phổ biến

Consumer<T>: Giao diện tiêu thụ

Đại diện cho việc nhận một tham số đầu vào và không có giá trị trả về.

Consumer<String> testConsumer = param -> System.out.println(param);
testConsumer.accept("testConsumer");

Consumer cung cấp andThen mặc định

Công dụng: Dùng để kết nối hai giao diện Consumer, một là giao diện Consumer của this khi gọi andThen, một là tham số after của andThen.

`con1.andThen(con2).accept(s);` tương đương với `con1.accept(s);con2.accept(s);`

Supplier<T>: Giao diện cung cấp

Không có tham số, trả về một kết quả.

Supplier<String> testSupplier = () -> String.valueOf(Math.random());
System.out.println(testSupplier.get());

Function<T, R>: Giao diện hàm

Nhận một tham số đầu vào, trả về một kết quả

Function testFunction = s -> s * 2;
System.out.println(testFunction.apply(6));

Predicate<T>: Giao diện断言

Nhận một tham số đầu vào, trả về một kết quả boolean.

Predicate<String> testPredicate = s -> s.equals("test");
System.out.println(testPredicate.test("test"));

Lưu ý:

  • Phương thức and có chức năng tương tự toán tử logic &&
  • Phương thức or có chức năng tương tự toán tử logic ||
  • Phương thức negate có chức năng tương tự toán tử logic !

Mỗi giao diện có một số phương thức mặc định, có thể kết hợp nhiều hiệu ứng hơn theo yêu cầu nghiệp vụ.

Giao diện hàm mở rộng

Mở rộng theo số lượng tham số:

Ví dụ, nhận hai tham số, có tiền tố Bi, như BiConsumer<T,U>, BiFunction<T,U,R>;

Biến thể đặc biệt thường dùng:

Ví dụ, BinaryOperator là BiFunction<T,T,T> cùng loại, toán tử nhị nguyên; UnaryOperator là Function<T,T> toán tử đơn nguyên.

Mở rộng theo kiểu dữ liệu:

Ví dụ, nhận tham số kiểu nguyên tử, như [Int|Double|Long] [Function|Consumer|Supplier|Predicate]

Tại sao cần mở rộng kiểu dữ liệu cơ bản

Chỉ kiểu đối tượng mới có thể là tham số tổng quát, đối với kiểu cơ bản sẽ liên quan đến việc đóng hộp và mở hộp, mặc dù là tự động, nhưng điều này không thể tránh khỏi gây ra overhead bộ nhớ bổ sung, đóng hộp và mở hộp đều gây overhead. Vì vậy, để giảm thiểu các overhead hiệu năng này, cần mở rộng kiểu dữ liệu cơ bản.

Trong Java 8, chỉ xử lý đặc biệt cho kiểu nguyên, kiểu dài và kiểu dấu phẩy động vì chúng được sử dụng nhiều nhất trong tính toán số học.

Các phương thức xử lý đặc biệt cho kiểu cơ bản có quy ước đặt tên rõ ràng:

  • Nếu tham số là kiểu cơ bản, không cần tiền tố, chỉ cần tên kiểu
  • Nếu kiểu trả về là kiểu cơ bản, thêm To trước kiểu cơ bản

Tóm lại: Thêm tiền tố kiểu [Int|Double|Long] biểu thị tham số là kiểu cơ bản, nếu thêm thêm To thì biểu thị kiểu trả về là kiểu cơ bản.

Phương thức tham chiếu

Phương thức tham chiếu có thể đơn giản hóa khai báo biểu thức lambda trong một số điều kiện nhất định. Cú pháp phương thức tham chiếu có ba loại:

  • objectName::instanceMethod
  • ClassName::staticMethod
  • ClassName::instanceMethod

Hai cách đầu tiên tương tự, tương đương với việc truyền tham số biểu thức lambda trực tiếp làm tham số của instanceMethod/staticMethod. Ví dụ, System.out::println tương đương với x->System.out.println(x); Math::max tương đương với (x, y)->Math.max(x,y).

Cách thứ ba, tương đương với việc truyền tham số đầu tiên của biểu thức lambda làm đối tượng đích của instanceMethod, các tham số còn lại làm tham số của phương thức đó. Ví dụ, String::toLowerCase tương đương với x -> x.toLowerCase().

//Function
Biểu thức Lambda: (Apple a) -> a.getWeight()
Tương đương phương thức tham chiếu: Apple::getWeight
//Consumer
Biểu thức Lambda: () -> Thread.currentThread().dumpStack()
Tương đương phương thức tham chiếu: Thread.currentThread()::dumpStack
//BiFunction
Biểu thức Lambda: (str, i) -> str.substring(i)
Tương đương phương thức tham chiếu: String::substring
//Function
Biểu thức Lambda: (String s) -> System.out.println(s)
Tương đương phương thức tham chiếu: System.out::print

Chỉ khi thân biểu thức lambda chỉ gọi một phương thức mà không thực hiện các thao tác khác, mới có thể chuyển biểu thức lambda thành phương thức tham chiếu. Ví dụ:

s -> s.length == 0

Ở đây có một lời gọi phương thức, nhưng còn có một phép so sánh, vì vậy không thể sử dụng phương thức tham chiếu ở đây.

Tham chiếu hàm tạo

Cú pháp tham chiếu hàm tạo như sau: ClassName::new, truyền tham số biểu thức lambda làm tham số của hàm tạo ClassName. Ví dụ, BigDecimal::new tương đương với x->new BigDecimal(x).

//Hàm tạo không tham số
Supplier<Apple> c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
//Hàm tạo một tham số
Function c2 = (weight) -> new Apple(weight);
Function c2 = Apple::new;
//Hàm tạo hai tham số
BiFunction c3 =(weight, color) -> new Apple(weight, color);
BiFunction c3 = Apple::new;

Sự khác biệt giữa biểu thức Lambda và lớp nội ẩn danh

1. Kiểu cần thiết khác nhau:

  • Lớp nội ẩn danh có thể là: giao diện, lớp trừu tượng, hoặc lớp cụ thể
  • Biểu thức Lambda chỉ có thể là: giao diện

2. Hạn chế sử dụng khác nhau:

  • Lớp nội ẩn danh: giao diện có thể có nhiều hoặc một phương thức
  • Biểu thức Lambda: yêu cầu giao diện chỉ có một phương thức

3. Nguyên lý thực hiện khác nhau:

  • Lớp nội ẩn danh: sau khi biên dịch sẽ tạo ra tệp .class riêng
  • Biểu thức Lambda: sau khi biên dịch không có tệp .class riêng, mã byte tương ứng sẽ được tạo động khi chạy.

Khác

Khi sử dụng vòng lặp trong biểu thức lambda, không thể sử dụng break và continue, sử dụng return (tương đương với chức năng break);

public static void main(String[] args) throws Exception {
    List<String> ss = new ArrayList<>();
    ss.add("aa");
    ss.add("bb");
    ss.add("cc");
    ss.forEach(c -> {
        System.out.println(c);
        return;
    });
    System.out.println("Thực thi hoàn thành");
}

Thẻ: Java 8 Lambda functional programming Stream API Functional Interface

Đăng vào ngày 25 tháng 5 lúc 14:15