Java OO & Functional
29/38
Bài 29 / 38~12 phútStream API & LambdaLộ trình · chặng 06/21Miễn phí… lượt xem

Method Reference và invokedynamic: Cơ Chế Bytecode Bên Dưới Lambda

Phân biệt 4 dạng method reference, cơ chế invokedynamic của JSR 292, LambdaMetafactory và khác biệt bộ nhớ giữa capturing và non-capturing lambda.

TL;DR: Method reference (::) là cú pháp rút gọn khi biểu thức lambda chỉ làm đúng nhiệm vụ chuyển tiếp (forward) tham số đến một phương thức có sẵn. Bên dưới bytecode, lambda không sinh ra file .class ẩn như Anonymous Inner Class. Trình biên dịch sinh lệnh invokedynamic (JSR 292), để JVM liên kết động qua LambdaMetafactory tại runtime. Nhờ cơ chế này, lambda non-capturing được tái sử dụng như một static singleton (zero-allocation), trong khi lambda capturing tạo đối tượng mới trên Heap mỗi lần thực thi để lưu trạng thái biến bao ngoài.

1. Method reference là gì và cú pháp :: hoạt động ra sao?

Sau khi đã tìm hiểu về biểu thức Lambda và Functional Interface, bạn sẽ nhận thấy có những trường hợp lambda chỉ đóng vai trò chuyển tiếp tham số thẳng vào một phương thức đã tồn tại. Khi đó, cú pháp Method Reference (::) cho phép ta loại bỏ hoàn toàn các tham số hình thức:

Java
// Lambda verbose:
Function<LibraryItem, String> getCodeLambda = item -> item.getBarcode();

// Method reference concise:
Function<LibraryItem, String> getCodeRef = LibraryItem::getBarcode;

Hai cách viết trên hoàn toàn tương đương về mặt ngữ nghĩa và kết quả thực thi. Trình biên dịch sẽ tự động suy luận kiểu và ánh xạ các tham số phù hợp với hợp đồng của Functional Interface đích.

2. Bốn dạng method reference phân biệt như thế nào?

Java hỗ trợ 4 dạng tham chiếu phương thức với cú pháp và ngữ cảnh sử dụng khác nhau:

Dạng tham chiếuCú phápVí dụ cụ thểTương đương Lambda
Static methodClassName::staticMethodInteger::parseInts -> Integer.parseInt(s)
Bound instance methodinstance::methodSystem.out::printlns -> System.out.println(s)
Unbound instance methodClassName::instanceMethodLibraryItem::getBarcodeitem -> item.getBarcode()
Constructor referenceClassName::newArrayList::new() -> new ArrayList<>()

Cây quyết định bốn dạng Method Reference phân biệt qua vế trái và vế phải cú pháp ::

Phân biệt Bound và Unbound Instance Method

Hai dạng này rất dễ gây nhầm lẫn:

  1. Bound instance method (instance::method): Tham chiếu được gắn chặt (bound) vào một thực thể cụ thể đã tồn tại từ trước:

    Java
    LibraryItem specialBook = new PrintedBook("SPECIAL-01", 1.2);
    Supplier<String> supplier = specialBook::getBarcode; // Bound vao specialBook
    
  2. Unbound instance method (ClassName::instanceMethod): Trước dấu :: là tên Class. Chưa có thực thể cụ thể nào được gán sẵn. Tham số đầu tiên của Functional Interface sẽ đóng vai trò là thực thể nhận cuộc gọi:

    Java
    // Tham so truyen vao lambda chinh la item de goi getBarcode()
    Function<LibraryItem, String> func = LibraryItem::getBarcode; 
    

Constructor Reference và Target Type

Constructor reference (ClassName::new) linh hoạt thay đổi constructor được gọi dựa theo Target Type:

  • Gán cho Supplier<List<String>>: Gọi default constructor ArrayList().
  • Gán cho Function<Integer, ArrayList<String>>: Gọi constructor ArrayList(int initialCapacity).

3. Khi nào nên dùng method reference thay cho lambda?

Khi nào NÊN dùng?

Khi biểu thức lambda chỉ gọi duy nhất một phương thức có sẵn mà không thực hiện bất kỳ phép biến đổi dữ liệu nào khác:

Java
// Readable and idiomatic:
items.stream().map(LibraryItem::getTitle).forEach(System.out::println);

Khi nào KHÔNG NÊN dùng?

Khi lambda thực hiện thêm logic tính toán, ghép chuỗi, hoặc kiểm tra điều kiện:

Java
// Keep lambda: contains custom formatting
items.stream().map(item -> "[" + item.getBarcode() + "] " + item.getTitle());

// Keep lambda: contains numeric logic
items.stream().map(item -> item.getDaysOverdue() * 2000.0);

[!TIP] Không nên cố gắng tạo ra các private method giả chỉ để ép buộc code sử dụng method reference. Tính rõ ràng (readability) và tính nhất quán luôn quan trọng hơn việc viết ngắn lại vài ký tự.

4. Tại sao lambda bên dưới bytecode là invokedynamic chứ không phải inner class?

Trước Java 8, các hàm nặc danh thường được hình dung sẽ dịch thành Anonymous Inner Class. Tuy nhiên, đội ngũ thiết kế JVM đã không chọn giải pháp này.

Sơ đồ tuần tự minh họa cơ chế invokedynamic liên kết CallSite ở lần đầu và nhảy thẳng ở các lần sau

Hệ quả nếu dịch thành Anonymous Inner Class

  • Metaspace Bloat: Trình biên dịch sẽ tạo ra hàng loạt file .class vật lý (CatalogService$1.class, CatalogService$2.class). ClassLoader phải nạp tất cả các file này vào bộ nhớ Metaspace.
  • Tốn chi phí cấp phát: Mỗi lần thực thi đều phải new một instance và chạy constructor của inner class, gây áp lực dọn rác lên Garbage Collector.

Giải pháp với invokedynamic (JSR 292)

Từ Java 8, compiler áp dụng hạ tầng invokedynamic:

  1. Compile-time: Logic của body lambda được trích xuất thành một private synthetic method (ví dụ lambda$demo$0()) ngay trong chính class hiện tại. Tại vị trí lambda, compiler sinh instruction invokedynamic trỏ tới bootstrap method LambdaMetafactory.metafactory.
  2. Runtime (Lần đầu): JVM kích hoạt LambdaMetafactory. Phương thức này sinh động một lightweight class ẩn trong RAM, cài đặt functional interface đích và trả về một CallSite chứa MethodHandle.
  3. Runtime (Các lần sau): JVM tái sử dụng CallSite đã liên kết, loại bỏ hoàn toàn chi phí bootstrap và cho phép JIT Compiler inline mã máy trực tiếp.

Bạn có thể tự kiểm chứng bằng công cụ dòng lệnh: javap -p -c CatalogService.class. Kết quả sẽ hiển thị chỉ thị invokedynamic cùng tham chiếu BootstrapMethods trong Constant Pool mà không hề có class nặc danh vật lý nào.

5. Capturing và non-capturing lambda khác biệt về cấp phát bộ nhớ ra sao?

Hiệu năng thực tế của lambda phụ thuộc trực tiếp vào việc lambda đó có nắm giữ biến từ môi trường bên ngoài hay không:

Java
// 1. Non-capturing lambda: Khong doc bien ngoai scope
for (int i = 0; i < 1_000_000; i++) {
    Consumer<LibraryItem> c = LibraryItem::getTitle; // Singleton: zero-allocation
}

// 2. Capturing lambda: Capture bien cuc bo prefix
for (int i = 0; i < 1_000_000; i++) {
    final String prefix = "LIB-" + i;
    Consumer<LibraryItem> c = item -> System.out.println(prefix + item.getBarcode()); 
    // Sinh 1 trieu doi tuong tren Heap!
}
  • Non-capturing lambda: Vì không mang trạng thái riêng, LambdaMetafactory khởi tạo lambda dưới dạng một static singleton instance. Mọi lần gọi qua dòng lệnh đều dùng chung đúng một object duy nhất (zero heap allocation).
  • Capturing lambda: Do cần lưu trữ giá trị của biến bên ngoài, JVM bắt buộc phải cấp phát một instance mới trên Heap trong mỗi vòng lặp để đóng gói dữ liệu (tương tự như truyền tham số vào trường dữ liệu của constructor).

6. Đâu là những cạm bẫy thường gặp với method reference?

❌ Cạm bẫy 1: Dùng method trả về void trong stream intermediate operation

Java
// Compile Error: Method println returns void, map requires a return value R
items.stream().map(System.out::println);

// Correct: Use forEach for terminal side-effects
items.stream().forEach(System.out::println);

❌ Cạm bẫy 2: Hiểu nhầm method reference tránh được NullPointerException

Java
LibraryItem item = null;
// NullPointerException ngay tai thoi diem bind method reference!
Runnable r = item::getTitle; 

Khi khai báo một Bound Instance Method Reference (item::getTitle), biểu thức item được đánh giá ngay lập tức tại thời điểm gán. Nếu item == null, chương trình ném NullPointerException ngay lập tức, chứ không đợi đến khi phương thức r.run() được gọi.

7. Tài liệu tham khảo sâu (Deep Dive)

📚 Deep Dive Oracle

8. Tóm tắt các điểm cốt lõi

  • Method reference :: giúp code gọn gàng khi lambda chỉ chuyển tiếp tham số vào một phương thức sẵn có.
  • 4 dạng: Static method (Integer::parseInt), Bound instance (specialItem::getBarcode), Unbound instance (LibraryItem::getBarcode), Constructor (ArrayList::new).
  • Java dịch lambda bằng chỉ thị invokedynamic và LambdaMetafactory ở runtime, không sinh file .class nặc danh.
  • Non-capturing lambda được lưu dưới dạng static singleton (zero-allocation); capturing lambda cấp phát đối tượng mới trên Heap.
  • Bound instance method reference đánh giá đối tượng nhận ngay tại thời điểm tạo, sẽ ném NPE lập tức nếu đối tượng bị null.

9. Câu hỏi tự kiểm tra (Self-Check)

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Method reference String::length tương đương với biểu thức lambda nào, và thuộc dạng tham chiếu gì?
  2. Q2
    Vì sao việc sử dụng invokedynamic giúp giảm áp lực bộ nhớ Metaspace so với Anonymous Inner Class?
  3. Q3
    Cùng một cú pháp ArrayList::new, tại sao trình biên dịch có thể gán cho cả Supplier lẫn Function?
  4. Q4
    Trong một vòng lặp lớn (1 triệu lần), tại sao nên ưu tiên non-capturing lambda hơn capturing lambda?
  5. Q5
    Đoạn mã sau có bị lỗi không nếu item mang giá trị null: Runnable r = item::getTitle;?

Bài tiếp theo: Stream Basics: Pipeline Lazy và Terminal Operation

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 6/21 · Lambda & Stream API — bài 3/12 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Stream Basics: Pipeline Lazy và Terminal Operation