Mở đầu chặng lập trình hàm. Trước khi tới Stream, cần hiểu thứ nằm dưới nó — và lambda không phải "cách viết tắt cho lớp ẩn danh" như nhiều người vẫn nghĩ.

Bắt đầu bằng một phép thử.

Gọi hai lần, nhận về cùng một đối tượng

static Runnable taoKhongBat() {
    return () -> System.out.println("không bắt gì");
}

Runnable a = taoKhongBat(), b = taoKhongBat();
  a == b ? true
  lớp     : Lam$$Lambda/0x0000000201000a18

Hai lời gọi, cùng một đối tượng. So với lớp ẩn danh:

  e == f ? false
  lớp     : Lam$1

Lớp ẩn danh luôn tạo đối tượng mới, và nó là một lớp thật — có file Lam$1.class trên đĩa.

Lambda thì không có file nào. Cái tên Lam$$Lambda/0x... cho thấy nó là lớp được sinh ra lúc chạy, và vì lambda này không phụ thuộc trạng thái gì nên JVM giữ lại một thể hiện dùng chung.

Đây là khác biệt thực chất đầu tiên: lambda không bắt biến gần như miễn phí, còn lớp ẩn danh thì mỗi lần một đối tượng.

Lambda có bắt biến thì khác

static Supplier<Integer> taoBatBienCucBo() {
    int x = 42;
    return () -> x;
}
  c == d ? false
  trường bên trong: [private final int Lam$$Lambda/0x...arg$1]

Giờ mỗi lần gọi là một đối tượng mới, và nó có một trường arg$1 chứa giá trị đã bắt.

So với lớp nội ở bài lớp lồng: lớp nội mang this$0tham chiếu tới cả đối tượng ngoài. Lambda chỉ mang đúng những giá trị nó cần. Đó là lý do lambda ít gây rò rỉ bộ nhớ hơn.

Nhưng có một ngoại lệ quan trọng, xem mục dưới.

Bytecode: invokedynamic cộng một phương thức riêng

  static java.lang.Runnable taoKhongBat();
    Code:
       0: invokedynamic #13,  0   // InvokeDynamic #0:run:()Ljava/lang/Runnable;
       5: areturn

Và trong danh sách phương thức của lớp:

  private java.lang.Integer lambda$thuTruong$5();
  private void lambda$thuThis$4();

Trình biên dịch làm hai việc: đưa thân lambda vào một phương thức private có tên sinh tự động, rồi thay chỗ dùng bằng một lệnh invokedynamic.

invokedynamic — ta đã gặp nó ở bài bytecode với phép nối chuỗi — nghĩa là "chỗ này cần một đối tượng cài Runnable, hãy để thư viện lúc chạy lo". Lần gọi đầu tiên, LambdaMetafactory sinh ra một lớp và gắn vào; những lần sau dùng lại.

Cách thiết kế này có lợi thật sự: file .class không phình ra vì lambda (không có file phụ như lớp ẩn danh), và JVM đời sau tối ưu tốt hơn thì mã cũ tự hưởng lợi mà không cần biên dịch lại.

this trỏ vào đâu

  lambda : this là Lam
  ẩn danh: this là          <- rỗng, vì lớp ẩn danh không có tên

Trong lambda, thisđối tượng của lớp bao ngoài. Trong lớp ẩn danh, this là chính thể hiện ẩn danh đó.

Đây là khác biệt gây bất ngờ khi chuyển mã cũ sang lambda. Với lớp ẩn danh, muốn gọi phương thức của lớp ngoài phải viết TenLopNgoai.this.phuongThuc(). Với lambda thì this đã là lớp ngoài rồi.

Cũng vì thế mà lambda không tự tham chiếu được — không viết được một lambda đệ quy trực tiếp. Cần đệ quy thì dùng phương thức thường, hoặc khai một biến trước rồi gán sau.

Effectively final: vì sao lại bắt buộc

error: local variables referenced from a lambda expression must be final or effectively final
        Runnable r = () -> System.out.println(dem);
                                              ^

Biến cục bộ nằm trên ngăn xếp và biến mất khi phương thức kết thúc. Lambda thì có thể sống lâu hơn — được trả về, lưu vào danh sách, chạy ở luồng khác.

Nên Java sao chép giá trị vào trường arg$1 của lambda. Và vì có hai bản, nếu cho phép gán lại thì chúng lệch nhau, sinh ra câu hỏi không có câu trả lời hay: lambda thấy bản nào?

Java chọn cách tránh hẳn: cấm gán lại. "Effectively final" nghĩa là bạn không cần viết final, chỉ cần không gán lại.

Biến chạy của vòng for cũng vi phạm, vì nó bị gán lại mỗi vòng:

error: ... must be final or effectively final
            Supplier<Integer> s = () -> i;

Cách vòng là sao ra một biến mới trong thân vòng lặp:

for (int i = 0; i < 3; i++) {
    int cuoiCung = i;
    ds.add(() -> cuoiCung);
}
  giá trị bắt được: 0 1 2

Mỗi lambda giữ một bản riêng. Với for-each thì không cần mẹo này — biến của for-each đã effectively final sẵn.

Trường thì khác hẳn

Supplier<Integer> s = () -> truongThuc;   // trường của đối tượng
truongThuc = 10;
  đặt trường = 10 rồi gọi: 10
  đặt trường = 20 rồi gọi: 20

Trường không bị luật effectively final ràng buộc, và lambda thấy giá trị mới nhất.

Lý do: lambda không sao chép giá trị của trường; nó bắt this rồi đọc trường mỗi lần chạy.

Hai hệ quả:

Lambda dùng trường sẽ giữ cả đối tượng bao ngoài sống — đúng vấn đề this$0 của bài lớp lồng, chỉ là qua đường khác. Một listener lambda đọc một trường sẽ neo giữ cả đối tượng chứa nó.

Kết quả phụ thuộc thời điểm gọi, không phải thời điểm tạo. Đây là nguồn của những lỗi khó tìm khi lambda được lưu lại rồi chạy sau.

Muốn "đóng băng" giá trị thì sao ra biến cục bộ trước:

int giaTriLucNay = truongThuc;
Supplier<Integer> s = () -> giaTriLucNay;

Cú pháp

Bốn dạng, từ đầy đủ tới rút gọn:

(String a, String b) -> { return a.length() - b.length(); }   // đủ
(a, b) -> { return a.length() - b.length(); }                 // bỏ kiểu
(a, b) -> a.length() - b.length()                             // một biểu thức
s -> s.isEmpty()                                              // một tham số, bỏ ngoặc
() -> System.out.println("x")                                 // không tham số

Vài luật nhỏ: bỏ kiểu thì phải bỏ hết, không bỏ nửa chừng. Một tham số thì bỏ được ngoặc, nhưng nếu ghi kiểu thì phải giữ ngoặc. Thân là một biểu thức thì giá trị của nó chính là giá trị trả về, không cần return.

Từ Java 11 dùng được var trong tham số — chủ yếu để đặt chú thích:

(@NonNull var a, @NonNull var b) -> a + b

Khi nào dùng lớp ẩn danh

Lambda thay được lớp ẩn danh trong đa số trường hợp, nhưng không phải tất cả:

Interface có nhiều phương thức trừu tượng — lambda chỉ dùng được với giao diện hàm.

Cần trạng thái riêng giữa các lần gọi — lớp ẩn danh có trường được, lambda thì không.

Cần this trỏ vào chính nó.

Cần đặt tên cho kiểu vì lý do gỡ lỗi — stack trace của lambda có tên như Lam.lambda$thuThis$4, khó đọc hơn tên lớp thật.

Thử ba mươi giây

Viết hai lambda giống hệt nhau, một cái không bắt biến nào, một cái bắt một biến cục bộ. So sánh bằng == hai lần gọi của mỗi loại.

Cái đầu cho true, cái sau cho false. Ba mươi giây đó cho thấy lambda không phải "đường cú pháp cho lớp ẩn danh" — nó là một cơ chế khác hẳn, và khác biệt đó đo được.

Ngày mai: các functional interface có sẵn — Function, Supplier, Consumer, Predicate và bộ biến thể cho kiểu nguyên thuỷ mà nhiều người không biết là có.