Type erasure — sự thật không dễ chịu của generics Java
Generics Java bị erase lúc compile, bytecode không biết T là gì; hệ quả: không new T(), không instanceof T, không catch T, heap pollution, và vì sao List<String> + List<Integer> cùng bytecode.
Generics Java trông giống C#, C++, TypeScript — nhưng cơ chế bên dưới khác hoàn toàn. C# reified generics giữ type info ở runtime. Java dùng type erasure — xoá mọi type parameter lúc compile, bytecode không biết List<String> khác List<Integer>.
Lý do: backward compatibility với code pre-Java 5. Code không generics vẫn chạy bên cạnh code generics mới mà không phá binary. Giá phải trả: nhiều hạn chế tinh vi bạn sẽ gặp khi viết generic code phức tạp.
Bài này giải thích cơ chế erasure, hệ quả thực tế, heap pollution, và workaround (Class<T> token, reflection).
1. Cơ chế — compiler xoá type parameter
Source:
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Sau compile, bytecode tương đương:
public class Box {
private Object value;
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}
T biến mất. Trong bytecode, T bị thay bằng:
Objectnếu không có bound.- Bound class nếu có
T extends X→ thay bằngX.
public class Sorted<T extends Comparable<T>> {
private T value;
// bytecode: private Comparable value;
}
1.1 Compiler thêm cast tự động
Client code:
Box<String> box = new Box<>();
box.set("hello");
String s = box.get();
Compile thành:
Box box = new Box(); // raw
box.set("hello");
String s = (String) box.get(); // CAST tu dong
Compiler chèn cast (String) tại mọi chỗ gọi get() — bạn không thấy nhưng bytecode có. Đây là lý do lỗi ClassCastException thỉnh thoảng xuất hiện ở dòng không có cast nào trong source — cast ẩn từ generics.
2. Hệ quả 1 — không new T()
public class Factory<T> {
public T create() {
return new T(); // COMPILE ERROR
}
}
Lý do: runtime T là Object — compiler không biết phải gọi constructor nào. Workaround đã nói ở bài 1: pass factory qua Supplier<T> hoặc Class<T>.
public class Factory<T> {
private final Supplier<T> supplier;
public Factory(Supplier<T> supplier) { this.supplier = supplier; }
public T create() { return supplier.get(); }
}
new Factory<>(User::new).create();
3. Hệ quả 2 — không instanceof T
public class Container<T> {
public boolean contains(Object obj) {
return obj instanceof T; // COMPILE ERROR
}
}
Runtime không biết T → không check được. Workaround: pass Class<T> token:
public class Container<T> {
private final Class<T> type;
public Container(Class<T> type) { this.type = type; }
public boolean contains(Object obj) {
return type.isInstance(obj);
}
}
Container<String> c = new Container<>(String.class);
c.contains("hello"); // true
c.contains(42); // false
Class.isInstance(obj) thay cho obj instanceof T — runtime check dựa vào Class<T> token bạn pass vào.
4. Hệ quả 3 — không generic array
T[] arr = new T[10]; // COMPILE ERROR
List<String>[] lists = new List<String>[10]; // COMPILE ERROR
Array Java biết kiểu runtime (int[], String[]) qua array covariance. Generic bị erase → tạo generic array phá type safety:
// Gia su duoc phep:
List<String>[] arr = new List<String>[1];
Object[] objArr = arr; // array covariance
objArr[0] = List.of(1, 2, 3); // Object[] nhan List<Integer> — bypass check
String s = arr[0].get(0); // ClassCastException — la Integer
Java cấm để ngăn pattern này. Workaround:
// Cach 1: dung List thay array
List<List<String>> lists = new ArrayList<>();
// Cach 2: cast raw array
@SuppressWarnings("unchecked")
List<String>[] arr = (List<String>[]) new List[10];
// WARNING "unchecked cast" — phai verify ban khong lam sai
Cách 1 đa số tốt hơn — rộng hơn, type-safe.
5. Hệ quả 4 — overload không khác nhau nếu erase ra cùng kiểu
public class Foo {
public void process(List<String> list) { }
public void process(List<Integer> list) { } // COMPILE ERROR
}
Cả 2 erase thành process(List) → cùng signature bytecode → conflict. Compiler bắt tại compile time.
Fix: đổi tên method hoặc đổi signature khác.
6. Hệ quả 5 — không catch generic exception
public class MyException<T> extends Exception { } // COMPILE ERROR
catch (MyException<String> e) ở runtime không phân biệt được với catch (MyException<Integer> e) — exception dispatch dựa vào class, erase làm mất thông tin. Java cấm generic trên Throwable.
7. Heap pollution — type safety bị phá ngầm
Erasure tạo kẽ hở: có thể nhét element sai kiểu vào List<String> mà không báo lỗi compile:
List<String> strings = new ArrayList<>();
List rawList = strings; // raw reference — warning unchecked
rawList.add(42); // compile OK, warning unchecked
String s = strings.get(0); // ClassCastException RUNTIME
rawList bypass type check; 42 lọt vào list. Khi get(0) returns Integer và compiler cast sang String → ClassCastException.
Đây gọi là heap pollution: tại runtime có object không khớp type parameter khai báo. JVM không phát hiện được (không có info), chỉ fail khi dùng.
7.1 Varargs + generic = heap pollution frequent
public static <T> List<T> asList(T... items) {
return Arrays.asList(items); // warning: unchecked
}
Varargs generic tạo T[] nội bộ — như đã nói, generic array không an toàn. Compiler warning "unchecked generic array creation". Fix: @SafeVarargs nếu bạn đảm bảo method không leak reference array ra ngoài.
8. Class<T> token — workaround phổ biến
Nhiều API muốn type runtime → nhận Class<T>:
public <T> T fromJson(String json, Class<T> type) {
// dung type.isInstance, type.newInstance, reflection
}
User u = json.fromJson(jsonString, User.class);
User.class là Class<User> object — chứa type info runtime. Jackson, Gson, JPA... đều dùng pattern này.
8.1 TypeToken / ParameterizedTypeReference — cho generic type
Class<T> không giữ được generic parameter: List<User>.class không hợp lệ. Workaround: anonymous subclass + reflection (Guava TypeToken, Spring ParameterizedTypeReference):
// Gson
Type listType = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, listType);
// Spring RestTemplate
ResponseEntity<List<User>> response = restTemplate.exchange(
url, GET, null, new ParameterizedTypeReference<List<User>>(){});
Anonymous subclass giữ được generic info qua reflection (JVM giữ với subclass info). Hack nhưng hoạt động.
9. Ưu điểm của erasure
Không phải toàn tệ — erasure có lý do:
- Backward compat: code pre-Java 5 chạy cùng code generic mới mà không phá binary. Quan trọng cho một ngôn ngữ dùng 20 năm.
- Bytecode nhỏ: một class
Listcho mọiT, không phải mỗiList<Integer>,List<String>một class bytecode. - Tương thích với JVM cũ: generics thêm vào JVM mà không thay đổi JVM spec.
C# reified generics đẹp hơn về runtime info, nhưng C# có toàn quyền breaking binary compat giữa version — Java không thể.
10. Pitfall tổng hợp
❌ Nhầm 1: Tưởng List<String> khác List<Integer> ở runtime.
if (list instanceof List<String>) { ... } // COMPILE ERROR
✅ Check List<?> chung hoặc lấy element rồi check kiểu.
❌ Nhầm 2: new T().
✅ Pass Supplier<T> hoặc Class<T>.
❌ Nhầm 3: Raw type làm bypass check.
List<String> strings = ...;
List raw = strings;
raw.add(42); // heap pollution
✅ Không dùng raw. @SuppressWarnings("unchecked") đánh dấu rõ khi cần.
❌ Nhầm 4: Generic exception.
class MyException<T> extends Exception { } // COMPILE ERROR
✅ Exception không generic; pass data qua field thường.
❌ Nhầm 5: Array generic.
T[] arr = new T[10]; // COMPILE ERROR
✅ List<T> thay array.
11. 📚 Deep Dive Oracle
Spec / reference chính thức:
- JLS §4.6 — Type Erasure — rule chính xác cách erase.
- JLS §4.8 — Raw Types — vì sao raw type tồn tại, rule sử dụng.
- JLS §4.12.2.1 — Heap Pollution — định nghĩa chính thức.
- Oracle Tutorial: Type Erasure.
- Project Valhalla — proposal cho reified generics + primitive generics.
- Effective Java Item 28: "Prefer lists to arrays"; Item 32: "Combine generics and varargs judiciously".
Ghi chú: Valhalla (đang phát triển) thử đưa "specialized generics" — List<int> không auto-box, generic parameter có runtime info. Nhưng phải giữ backward compat với code hiện tại — công việc khổng lồ. Chưa có timeline cụ thể cho LTS.
12. Tóm tắt
- Type erasure: compiler xoá type parameter
T, thay bằngObject(hoặc bound). Bytecode không biết T là gì. - Compiler tự chèn cast
(T)tại call site — bạn không thấy nhưng runtime có. - Hệ quả:
new T(),instanceof T,T[], generic exception — đều không làm được. - Heap pollution: qua raw type, có thể đưa object sai kiểu vào
List<T>— ClassCastException ở chỗget()không có cast rõ. - Workaround:
Class<T>token,Supplier<T>,TypeToken/ParameterizedTypeReference. - Overload 2 method có signature erase giống nhau → compile error.
- Lý do erasure: backward compat với code pre-Java 5 + bytecode nhỏ.
- Project Valhalla đang thử sửa — chưa production.
13. Tự kiểm tra
Q1Đoạn sau compile không? Nếu có, chạy ra gì?List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
▸
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());Compile OK, runtime in true.
Sau type erasure, cả ArrayList<String> và ArrayList<Integer> đều là class java.util.ArrayList ở runtime. getClass() trả cùng Class object.
Hệ quả: không có cách phân biệt List<String> với List<Integer> ở runtime. instanceof chỉ check được List (raw) hoặc List<?> (wildcard), không check được type parameter.
Q2Đoạn sau khi nào ném ClassCastException và tại sao?List<String> strings = new ArrayList<>();
List raw = strings;
raw.add(42);
String s = strings.get(0);
System.out.println(s.length());
▸
List<String> strings = new ArrayList<>();
List raw = strings;
raw.add(42);
String s = strings.get(0);
System.out.println(s.length());Ném ClassCastException tại dòng String s = strings.get(0) — dù dòng đó trông không có cast.
Flow:
rawlà reference raw type tới cùng list → bypass generic check.raw.add(42)compile OK (raw type không check), runtime:Integer(42)bị add vào list.strings.get(0)— bytecode làlist.get(0)returnObject. Compiler đã chèn cast(String)tự động → tại runtime castIntegersangStringfail.- Stack trace trỏ đến dòng
strings.get(0), không trỏ đến dòngraw.add(42)— đây là lý do heap pollution khó debug: lỗi xảy ra xa nơi gây.
Bài học: không dùng raw type, không bỏ qua warning "unchecked".
Q3Vì sao đoạn sau compile error?public class Factory {
public void process(List<String> list) { }
public void process(List<Integer> list) { }
}
▸
public class Factory {
public void process(List<String> list) { }
public void process(List<Integer> list) { }
}Sau type erasure, cả hai method đều erase thành process(List list) → signature giống nhau ở bytecode → conflict. Compiler bắt lỗi "name clash: process(List) and process(List) have the same erasure".
Fix (chọn 1):
- Đổi tên method:
processStrings(...),processIntegers(...). - Merge thành generic:
public <T> void process(List<T> list)— nếu logic không phụ thuộc kiểu cụ thể. - Gộp bằng super bound:
public void process(List<? extends Number> list)— nhận cảList<Integer>,List<Double>... (bài 3).
Q4Viết method generic nhận Class<T> token để check runtime kiểu:public static <T> List<T> filterByType(List<?> list, Class<T> type) { ... }
▸
Class<T> token để check runtime kiểu:public static <T> List<T> filterByType(List<?> list, Class<T> type) { ... }public static <T> List<T> filterByType(List<?> list, Class<T> type) {
List<T> result = new ArrayList<>();
for (Object o : list) {
if (type.isInstance(o)) {
result.add(type.cast(o));
}
}
return result;
}
// Su dung:
List<Object> mixed = List.of("hello", 42, "world", 3.14, "java");
List<String> strings = filterByType(mixed, String.class);
// strings = ["hello", "world", "java"]Pattern này tận dụng Class<T> làm runtime type token để bù đắp erasure:
type.isInstance(o)thay thế choo instanceof T(cái này không viết được).type.cast(o)thay thế cho(T) o— type-safe, némClassCastExceptionrõ ràng nếu sai.
Đây là pattern Jackson/Gson dùng: objectMapper.readValue(json, User.class) — token User.class mang type info vào runtime.
Q5Đoạn sau có ý đồ khai List<Integer>[] nhưng không compile được. Tại sao và cách thay thế?List<Integer>[] lists = new List<Integer>[10];
▸
List<Integer>[] nhưng không compile được. Tại sao và cách thay thế?List<Integer>[] lists = new List<Integer>[10];Compile error: "generic array creation". Java cấm tạo array với element type có generic parameter vì array + generics + covariance mở ra lỗ hổng type safety:
// Neu cho phep:
List<Integer>[] a = new List<Integer>[1];
Object[] b = a; // array covariance: Subtype[] -> Supertype[]
b[0] = List.of("x"); // Object[] nhan List<String> — khong check gi
Integer n = a[0].get(0); // ClassCastException — thuc ra la StringThay thế (chọn theo use case):
List<List<Integer>> lists = new ArrayList<>();— dùng List thay array. Gần như luôn là lựa chọn tốt nhất — type-safe và flexible hơn.- Nếu thật cần array:Compile với warning unchecked — đánh dấu
@SuppressWarnings("unchecked") List<Integer>[] lists = (List<Integer>[]) new List[10];@SuppressWarningskhi bạn đảm bảo không leak ra nơi gây heap pollution.
Effective Java Item 28: "Prefer lists to arrays" — nhiều lý do, type erasure là một.
Bài tiếp theo: Wildcard ? extends, ? super và PECS rule
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên