Java — Từ Zero đến Senior/Generics & Collections/Type erasure — sự thật không dễ chịu của generics Java
2/5
~18 phútGenerics & Collections

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:

  • Object nếu không có bound.
  • Bound class nếu có T extends X → thay bằng X.
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 TObject — 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.classClass<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 List cho mọi T, không phải mỗi List<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:

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ằng Object (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

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());

Compile OK, runtime in true.

Sau type erasure, cả ArrayList<String>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());

Ném ClassCastException tại dòng String s = strings.get(0) — dù dòng đó trông không có cast.

Flow:

  1. raw là reference raw type tới cùng list → bypass generic check.
  2. raw.add(42) compile OK (raw type không check), runtime: Integer(42) bị add vào list.
  3. strings.get(0) — bytecode là list.get(0) return Object. Compiler đã chèn cast (String) tự động → tại runtime cast Integer sang String fail.
  4. Stack trace trỏ đến dòng strings.get(0), không trỏ đến dòng raw.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".

Q3
Vì sao đoạn sau compile error?
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).
Q4
Viết method generic nhận 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ế cho o instanceof T (cái này không viết được).
  • type.cast(o) thay thế cho (T) o — type-safe, ném ClassCastException rõ 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];

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 String

Thay 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:
    @SuppressWarnings("unchecked")
    List<Integer>[] lists = (List<Integer>[]) new List[10];
    Compile với warning unchecked — đánh dấu @SuppressWarnings khi 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