Type erasure — bản chất thực thi của generics Java
Cơ chế Type Erasure xóa tham số kiểu ở bytecode, cơ chế sinh Bridge Method, các hạn chế kỹ thuật và giải pháp Type Token Class<T> trong Java.
TL;DR: Generics trong Java hoạt động theo nguyên lý Type Erasure (Xoá kiểu): trình biên dịch kiểm tra tính an toàn kiểu tại thời điểm build, nhưng sau đó sẽ xóa bỏ toàn bộ thông tin tham số kiểu và thay thế bằng Object (hoặc lớp ràng buộc bound) trong file bytecode. Máy ảo JVM ở runtime không hề biết sự khác biệt giữa List<String> và List<Integer>. Để duy trì tính đa hình, compiler tự động sinh các Bridge Methods ngầm định.
Ở bài trước, bạn đã thấy Generics mang lại sự an toàn kiểu tuyệt đối ở giai đoạn biên dịch. Nhưng bạn có tự hỏi: Tại sao Java không hỗ trợ List<int>, và tại sao ta không thể kiểm tra if (item instanceof T)? Bí mật nằm ở cơ chế Type Erasure mà chúng ta sẽ mổ xẻ ngay sau đây.
Khác với C# (sử dụng Reified Generics giữ nguyên thông tin kiểu trong Runtime) hay C++ (sử dụng Templates sinh mã riêng cho từng kiểu dữ liệu), Java chọn con đường Type Erasure. Lựa chọn này đánh đổi tính năng để lấy tính tương thích ngược: các thư viện viết từ Java 1.4 vẫn có thể chạy liền mạch bên cạnh mã nguồn Java hiện đại.
1. Trình biên dịch xóa tham số kiểu và chèn mã ép kiểu như thế nào?
Quan sát mã nguồn bạn viết:
public class ItemBox<T> {
private T item;
public void store(T item) { this.item = item; }
public T retrieve() { return item; }
}
Sau khi chạy lệnh javac, mã bytecode nhị phân thực tế tương đương với:
// Bytecode ma JVM thuc su nhin thay:
public class ItemBox {
private Object item; // T da bi bien thanh Object!
public void store(Object item) { this.item = item; }
public Object retrieve() { return item; }
}
Nếu tham số kiểu có ràng buộc T extends LibraryItem, T sẽ bị biến đổi thành LibraryItem thay vì Object.
Compiler tự động chèn lệnh ép kiểu ngầm
Khi client sử dụng:
ItemBox<PrintedBook> box = new ItemBox<>();
box.store(new PrintedBook("B-01", "Clean Code", 400, 500.0));
PrintedBook book = box.retrieve();
Trình biên dịch biến đổi dòng cuối thành:
PrintedBook book = (PrintedBook) box.retrieve(); // LENH EP KIEU DUOC CHEN TU DONG

Đây là lý do đôi khi bạn gặp ngoại lệ ClassCastException tại một dòng mã hoàn toàn không có dấu ép kiểu nào: lỗi đó xuất phát từ chính lệnh cast ẩn do compiler tự động chèn vào.
2. Bridge Method duy trì tính đa hình ở runtime bằng cách nào?
Khi một lớp con kế thừa một generic class với kiểu cụ thể:
public class Node<T> {
public void setData(T data) {
System.out.println("Node.setData");
}
}
public class BookNode extends Node<PrintedBook> {
@Override
public void setData(PrintedBook data) {
System.out.println("BookNode.setData: " + data.getTitle());
}
}
Sau khi Type Erasure diễn ra:
- Lớp cha có phương thức:
setData(Object data). - Lớp con có phương thức:
setData(PrintedBook data).
Về mặt chữ ký nhị phân của JVM, đây là hai phương thức nạp chồng (Overload) chứ không phải ghi đè (Override). Nếu gọi qua tham chiếu cha Node n = new BookNode(); n.setData(new Object()), đa hình sẽ bị gãy.
Để giải quyết mâu thuẫn này, trình biên dịch tự động sinh một phương thức cầu nối (Bridge Method) trong bytecode của BookNode:
// Bridge Method duoc compiler tu dong sinh ngam trong BookNode:
public void setData(Object data) {
this.setData((PrintedBook) data); // Ep kieu ve PrintedBook roi chuyen tiep
}

Bạn có thể kiểm chứng sự tồn tại của Bridge Method bằng lệnh javap -c -p BookNode.class (phương thức này được gắn cờ ACC_BRIDGE và ACC_SYNTHETIC).
3. Tại sao Java cấm toán tử new T() và cách khắc phục?
public class AssetFactory<T> {
public T createInstance() {
return new T(); // COMPILE ERROR: Cannot instantiate the type T
}
}
Do Type Erasure, lúc chạy T chỉ là Object. Máy ảo không thể biết T là lớp gì, có hàm khởi tạo không tham số hay không để cấp phát vùng nhớ.
Giải pháp: Sử dụng Factory Supplier hoặc Type Token Class<T>
// Giai phap 1: Dung Supplier Functional Interface
public class AssetFactory<T> {
private final Supplier<T> creator;
public AssetFactory(Supplier<T> creator) {
this.creator = creator;
}
public T createInstance() {
return creator.get();
}
}
// Su dung cuc ky linh hoat:
AssetFactory<PrintedBook> factory = new AssetFactory<>(() -> new PrintedBook("B-01", "Java", 200, 300.0));
4. Tại sao không thể kiểm tra instanceof T và giải pháp Type Token?
public boolean isMatching(Object obj) {
return obj instanceof T; // COMPILE ERROR: Cannot perform instanceof check against type parameter T
}
Tại runtime, thông tin T đã biến mất, nên JVM không thể thực hiện kiểm tra kiểu.
Giải pháp: Truyền Class<T> Token
public class TypeChecker<T> {
private final Class<T> typeToken;
public TypeChecker(Class<T> typeToken) {
this.typeToken = typeToken;
}
public boolean isMatching(Object obj) {
return typeToken.isInstance(obj); // Kiem tra an toan luc chay
}
}
TypeChecker<PrintedBook> checker = new TypeChecker<>(PrintedBook.class);
boolean match = checker.isMatching(someObject);
Đây là kỹ thuật kinh điển được các framework như Jackson, Spring, GSON sử dụng để giải mã JSON thành đúng đối tượng Generic.
5. Tại sao Java cấm khởi tạo mảng Generic new T[10]?
T[] items = new T[10]; // COMPILE ERROR: Cannot create a generic array of T
Mảng trong Java là Covariant (đồng biến) và Reified (lưu giữ kiểu lúc chạy): mảng String[] luôn nhớ nó là mảng String và sẽ ném lỗi ArrayStoreException nếu bạn nhét số nguyên vào. Ngược lại, Generics là Invariant (bất biến) và Erased (bị xoá kiểu). Sự đối lập này khiến việc tạo mảng generic trở nên không an toàn:
![Hai cột chỉ ra xung đột: mảng là reified và covariant trong khi generic là erased và invariant, cho phép nhét Integer vào mảng String qua vỏ Object[]](/images/diagrams/java-oo-functional/03-generics-va-collections/04-mang-generic-choi-nhau-the-nao@2x.drawio.png)
Nếu cho phép new T[10], mã nguồn sẽ bị hiện tượng nhiễm bẩn vùng nhớ (Heap Pollution), dẫn đến ClassCastException tại những dòng mã hoàn toàn hợp lệ. Đó là lý do Effective Java khuyên luôn dùng List<T> thay vì T[].
6. Deep Dive Oracle & Tài liệu tham khảo
- JLS §4.6 (Type Erasure): Quy tắc kỹ thuật chính thức về cách trình biên dịch ánh xạ các kiểu tham số hoá về kiểu thô (raw types).
- JVMS §4.7.9 (The Signature Attribute): Cách JVM lưu trữ metadata generic trong bytecode để phục vụ công cụ Reflection (dù runtime bytecode vẫn chạy kiểu erased).
- Effective Java (Joshua Bloch) Item 28: "Prefer lists to arrays" (Ưu tiên danh sách hơn mảng để tránh xung đột giữa Reified Array và Erased Generics).
7. Tự kiểm tra kiến thức
- Q1Tại sao hai biểu thức List<String> và List<Integer> có cùng một đối tượng Class tại thời điểm chạy?
- Q2Bridge Method trong Java được sinh ra để giải quyết bài toán kỹ thuật nào?
- Q3Làm thế nào để khởi tạo một đối tượng thuộc kiểu tham số generic T bên trong một lớp generic?
- Q4Tại sao Java cấm khai báo cú pháp tạo mảng generic như new T[10]?
- Q5Tại sao Java không chọn giải pháp Reified Generics như ngôn ngữ C#?
Bài tiếp theo: Wildcard và PECS — producer extends consumer super
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.
Generics cơ bản — class và method nhận kiểu làm tham số
Bài tiếpWildcard `? extends`, `? super` — PECS rule
Lộ trình Junior Java Backend Developer
Chặng 5/21 · Generics & Collections — bài 2/10 của chặng
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Thảo luận & góp ý
Hỏi đáp về bài này