Java 泛型擦除機制導致運行時類型信息丟失,配合反射可繞過編譯期類型檢查,將錯誤類型對象放入泛型集合,最終在取出時拋出 ClassCastException。本文從字節碼、類型擦除、反射機制三個層面深度剖析。
ArrayList<Integer> list3 = new ArrayList<Integer>();
list3.add(1); // 編譯期檢查:只能放 Integer
// 反射繞過編譯檢查
list3.getClass().getMethod("add", Object.class).invoke(list3, "asd"); // 放入 String!
for (int i = 0; i < list3.size(); i++) {
System.out.println(list3.get(i)); // 這裡正常輸出 1, asd
}
// 反轉操作
ArrayList<String> list2 = new ArrayList<String>();
list2.add("asd");
list2.getClass().getMethod("add", Object.class).invoke(list2, 1); // 放入 Integer
for (int i = 0; i < list2.size(); i++) {
System.out.println(list2.get(i)); // 拋出 ClassCastException: Integer cannot be cast to String
}
1
asd
asd
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Fanxing.main(Fanxing.java:19)
| 階段 | ArrayList<Integer> | ArrayList<String> |
|---|---|---|
| 源碼 | List<Integer> | List<String> |
| 字節碼 | List (原始類型) | List (原始類型) |
| add 方法簽名 | add(Object) | add(Object) |
| get 方法簽名 | Object get(int) | Object get(int) |
關鍵點:泛型類型參數在編譯後被「擦除」為 Object (或邊界類型),運行時無法區分 List<Integer> 和 List<String>。
// list3 實際類型:ArrayList (擦除後)
// 存儲內容:[Integer(1), String("asd")]
for (int i = 0; i < list3.size(); i++) {
System.out.println(list3.get(i)); // 編譯器插入隱式轉型:(Integer) list3.get(i)
}
System.out.println(Object) 接受任意對象,無需轉型為 Integer。JVM 調用 Object.toString(),Integer 和 String 都正常實現,故不報錯。
// list2 實際類型:ArrayList (擦除後)
// 存儲內容:[String("asd"), Integer(1)]
for (int i = 0; i < list2.size(); i++) {
System.out.println(list2.get(i)); // 編譯器插入隱式轉型:(String) list2.get(i)
}
編譯器根據聲明類型 List<String>,在 get(i) 後插入 checkcast #String 字節碼指令。第 2 次循環取出 Integer,執行 checkcast 失敗,拋出 ClassCastException。
; list3.get(i) 調用點
invokeinterface java/util/List.get:(I)Ljava/lang/Object;
invokestatic java/lang/System.out.println:(Ljava/lang/Object;)V ; 直接打印 Object
; list2.get(i) 調用點
invokeinterface java/util/List.get:(I)Ljava/lang/Object;
checkcast java/lang/String ; 強制轉型 String
invokestatic java/lang/System.out.println:(Ljava/lang/String;)V ; 打印 String 重載
Method m = list.getClass().getMethod("add", Object.class); // 獲取擦除後的 add(Object)
m.invoke(list, "asd"); // 運行時傳入 String,JVM 僅檢查參數是否為 Object 子類型 → 通過
反射調用繞過編譯器,直接調用運行時方法,而運行時方法簽名為 add(Object),任意對象均可傳入。
TypeToken<List<Integer>>() (Gson) 保留類型信息用於反序列化-Xlint:unchecked,關注「unchecked call」警告// 安全的異構集合處理模式
public <T> List<T> safeCastList(List<?> rawList, Class<T> clazz) {
List<T> result = new ArrayList<>();
for (Object obj : rawList) {
if (clazz.isInstance(obj)) {
result.add(clazz.cast(obj));
} else {
// 記錄/拋出/跳過 類型不匹配項
}
}
return result;
}
泛型擦除使運行時集合無類型參數信息,反射可繞過編譯期檢查向其中放入任意對象。System.out.println(Object) 掩蓋了第一個列表的類型污染,而編譯器為第二個列表插入的 checkcast 指令在取出錯誤類型時觸發異常。這揭示了類型安全邊界在編譯期而非運行期,反射打破邊界需極謹慎。