Java 泛型是編譯期語法糖,運行期通過類型擦除實現。這導致 ArrayList<String> 和 ArrayList<Integer> 在 JVM 中共享同一個 Class 對象。本文從設計動機、擦除機制、代碼演示、副作用與應對策略五個維度,系統闡述這一核心機制。
Class<?> c1 = new ArrayList<String>().getClass(); Class<?> c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 + " == " + c2 + " is " + (c1 == c2)); // 輸出:class java.util.ArrayList == class java.util.ArrayList is true
無論泛型參數為何,運行期獲取的 Class 對象完全相同。
Object,需手動強轉ArrayList_String、ArrayList_Integer 等獨立類,破壞二進制兼容<T>) → 擦除為 Object<T extends Number>) → 擦除為第一個界限(Number)源碼:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
擦除後等效字節碼邏輯:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 編譯器插入 CHECKCAST 指令
class Pair<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// 擦除後
class Pair {
private Object value;
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}
若子類繼承泛型父類並具體化類型,編譯器生成橋接方法保持多態:
class StringPair extends Pair<String> {
// 編譯器生成橋接方法
public void set(Object value) { set((String) value); } // 橋接到 set(String)
public String get() { return (String) super.get(); }
}
| 現象 | 原因 | 應對方案 |
|---|---|---|
泛型類無獨有 Class 對象 |
運行期類型參數已擦除 | 使用 TypeToken (Gson) 或傳遞 Class<T> 參數 |
| 靜態成員被所有實例共享 | 類級屬性不屬於泛型實例 | 避免在泛型類中聲明靜態泛型字段 |
| 無法在 catch 中使用泛型異常 | JVM 運行期無法區分 MyEx<String> 和 MyEx<Integer> |
異常類不使用泛型參數,或用非泛型基類捕獲 |
無法 new T() / new T[] |
運行期不知 T 具體類型 | 工廠模式、反射構造、傳遞 Class<T> |
無法 instanceof T / T.class |
類型信息不存在於運行期 | 使用 ParameterizedType 獲取泛型簽名 |
| 重載方法擦除後簽名衝突 | void m(List<String>) 和 void m(List<Integer>) 擦除同名 |
方法重命名、不同參數類型、單一方法內部判斷 |
// Gson 風格
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
Type type = token.getType(); // 保留 ParameterizedType 信息
原理:匿名內部類繼承 TypeToken<List<String>>,其 getGenericSuperclass() 返回帶泛型參數的 ParameterizedType。
class Container {
List<String> items;
Map<Integer, User> cache;
}
Field f = Container.class.getDeclaredField("items");
ParameterizedType pt = (ParameterizedType) f.getGenericType();
Type[] args = pt.getActualTypeArguments(); // [class java.lang.String]
public <T> T parse(String json, Class<T> clazz) {
return gson.fromJson(json, clazz);
}
// 調用
User user = parse(json, User.class);
List<User> list = parse(json, new TypeToken<List<User>>(){}.getType());
List<String> 和 List<Integer> 運行期同為 ListClass<T> 傳遞、TypeToken、反射 ParameterizedType 在需要時「找回」類型信息深刻理解類型擦除,是寫出健壯泛型代碼、避開「運行期類型丟失」坑的前提。