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:
// 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ếu | Cú pháp | Ví dụ cụ thể | Tương đương Lambda |
|---|---|---|---|
| Static method | ClassName::staticMethod | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound instance method | instance::method | System.out::println | s -> System.out.println(s) |
| Unbound instance method | ClassName::instanceMethod | LibraryItem::getBarcode | item -> item.getBarcode() |
| Constructor reference | ClassName::new | ArrayList::new | () -> new ArrayList<>() |

Phân biệt Bound và Unbound Instance Method
Hai dạng này rất dễ gây nhầm lẫn:
-
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:JavaLibraryItem specialBook = new PrintedBook("SPECIAL-01", 1.2); Supplier<String> supplier = specialBook::getBarcode; // Bound vao specialBook -
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 constructorArrayList(). - Gán cho
Function<Integer, ArrayList<String>>: Gọi constructorArrayList(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:
// 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:
// 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.

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
.classvậ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
newmộ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:
- 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 instructioninvokedynamictrỏ tới bootstrap methodLambdaMetafactory.metafactory. - 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ộtCallSitechứaMethodHandle. - 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:
// 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,
LambdaMetafactorykhở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
// 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
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)
- JLS §15.13 Method Reference: Đặc tả chi tiết 4 dạng tham chiếu phương thức và quy tắc giải quyết overload.
- Java API LambdaMetafactory: Lớp sinh implementation lambda động trong runtime.
- Translation of Lambda Expressions by Brian Goetz: Tài liệu kiến trúc của trưởng nhóm thiết kế Java phân tích chi tiết vì sao
invokedynamicvượt trội hơn anonymous inner class.
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ị
invokedynamicvàLambdaMetafactoryở runtime, không sinh file.classnặ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)
- Q1Method reference String::length tương đương với biểu thức lambda nào, và thuộc dạng tham chiếu gì?
- Q2Vì sao việc sử dụng invokedynamic giúp giảm áp lực bộ nhớ Metaspace so với Anonymous Inner Class?
- Q3Cù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?
- Q4Trong 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?
- 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.
Lambda và Functional Interface trong Java
Bài tiếpStream Basics: Pipeline Lazy và Terminal Operation
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