在 ArrayList<E> 中,toArray 方法聲明為:
public <T> T[] toArray(T[] a)
而非直覺的:
public E[] toArray(E[] a) // 看似更類型安全
這為什麼?是 JDK 設計失誤,還是有深層考量?
// 陣列協變:Number[] 是 Object[] 的子型
Number[] numArr = new Integer[10]; // 編譯通過
numArr[0] = new Double(1.0); // 執行時拋 ArrayStoreException
// 泛型不變:List<Number> 不是 List<Integer> 的父型
List<Number> numList = new ArrayList<Integer>(); // 編譯錯誤
若 toArray 使用 E[],呼叫端傳入 Number[] 接收 ArrayList<Integer> 結果時,編譯器會報錯,失去了「向上轉型」的靈活性。
因類型擦除,運行時 E 變為 Object,無法 new E[]。現有實現依賴傳入陣列的 getClass() 反射創建同類型新陣列:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
若簽名為 E[],呼叫端傳 new Number[0] 時,a.getClass() 返回 Number[],複製出的陣列類型正確;但編譯器會強制要求傳入 Integer[],限制了用法。
List<Integer> intList = Arrays.asList(1, 2, 3);
// 1. 向上轉型:子類 List 輸出父類陣列 ✅
Number[] numArr = intList.toArray(new Number[0]);
// 2. 介面轉型:List 輸出 Object 陣列 ✅
Object[] objArr = intList.toArray(new Object[0]);
// 3. 同類型輸出 ✅
Integer[] intArr = intList.toArray(new Integer[0]);
若簽名為 E[],第 1、2 種寫法編譯直接失敗。
<T super E>?Java 泛型不支援 super 用於方法類型參數(僅限通配符 <? super E>)。且陣列協變特性導致即使加上界也無法完全阻擋 ArrayStoreException:
// 反例:即使 T 是 E 父類,運行時仍可能拋異常
List<Integer> list = new ArrayList<>();
Float[] floatArr = new Float[2];
Number[] numArr = floatArr; // 合法:Float[] 是 Number[] 子型
list.toArray(numArr); // 執行時 ArrayStoreException:Integer 不能存入 Float[]
toArray(Object[]) 存在於 Java 1.2(1998),泛型於 Java 5(2004)引入。為保持二進制兼容,遷移時僅能將 Object 泛化為 <T> T,無法改變語義。
toArray(new T[size]):指定大小避免反射創建新陣列,效能最優。
String[] arr = list.toArray(new String[list.size()]); // 推薦
toArray(new T[0]):雖 Java 6+ 優化過(逃逸分析可消除分配),但語義不明確。toArray(IntFunction<T[]> generator) 完全解決類型推斷痛點:
String[] arr = list.toArray(String[]::new); // 類型安全、簡潔、高效
ArrayList.toArray(T[]) 使用獨立類型參數 T 而非類參數 E,是Java 類型系統(陣列協變+泛型不變+類型擦除)妥協下的最優解。它犧牲了編譯期類型安全(可能拋 ArrayStoreException),換取了運行時靈活性(支援向上轉型)與歷史兼容性。現代 Java 開發建議使用 toArray(IntFunction) 徹底規避此問題。