Java OO & Functional/Type erasure — sự thật không dễ chịu của generics Java
16/33
Bài 16 / 33~18 phútGenerics & CollectionsMiễn phí lượt xem

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

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

Đặt 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