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
ℹ️ 📚 Deep Dive Oracle (optional)
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