翻閱 ArrayList<E> 已聲明元素類型 E,為何 toArray 方法要重新聲明類型參數 <T> T[] toArray(T[] a),而不是直接用 E[] toArray(E[] a)?本文從類型安全、靈活性、歷史兼容三個維度還原設計決策。
List<String> list = new ArrayList<>();
list.add("a"); list.add("b");
// 假設簽名為 E[] toArray(E[] a)
String[] strArr = list.toArray(new String[0]); // ✅ 編譯通過、運行正常
// 若想轉為父類型數組
Number[] numArr = list.toArray(new Number[0]); // ❌ 編譯報錯:String[] 非 Number[] 子類型
若強制用 E,上述「子類 List 轉父類數組」的合理需求在編譯期就被阻擋,失去泛型協變帶來的靈活性。
Java 數組是協變的:Integer[] 是 Number[] 的子類型。
T 允許調用者傳入 父類型數組,運行時檢查實際元素是否兼容:
List<Integer> ints = Arrays.asList(1, 2);
Number[] nums = ints.toArray(new Number[0]); // ✅ 運行期通過,返回 Number[]
若簽名為 E[],則 Number[] 無法傳入 E[](此處 E=Integer),直接編譯失敗。
即使改為 <T super E> T[] toArray(T[] a)(Java 不支援下界通配符於方法類型參數),數組協變特性仍會導致運行時拋異常:
// 代碼一:T 是 E 父類
List<Integer> list = new ArrayList<>();
Float[] floats = new Float[2];
Number[] nums = floats; // 數組協變:Float[] → Number[]
list.toArray(nums); // ❌ ArrayStoreException:Integer 存不進 Float[]
// 代碼二:T == E
List<Number> list2 = new ArrayList<>();
list2.add(new Integer(1));
Float[] floats2 = new Float[2];
Number[] nums2 = floats2;
list2.toArray(nums2); // ❌ 同理拋異常
結論:無論簽名如何,運行時數組存儲檢查(JVM 指令 aastore)才是最終防線,編譯期無法完全消除風險。
// JDK 1.4
public Object[] toArray(Object[] a);
// JDK 5 泛型化:擦除後簽名仍為 Object[] toArray(Object[])
// 若改為 E[] toArray(E[] a),擦除後變為 Object[] toArray(Object[]) —— 看似兼容,
// 但橋接方法生成、二進制兼容性、序列化兼容性會引入極大風險。
// 現有寫法 T[] toArray(T[] a) 擦除後同為 Object[] toArray(Object[]),
// 完美保持二進制兼容,舊字節碼無需重編譯即可運行。
Arrays.copyOf 重新分配
List<Integer> list = ...;
Integer[] arr = list.toArray(new Integer[list.size()]); // 零拷貝最快
// 或 Java 11+ 更簡潔
Integer[] arr = list.toArray(Integer[]::new);
NullPointerExceptiona.length >= size:元素存入 a,a[size] = null(若還有剩餘),返回 aa.length < size:新分配 T[](a.getClass() 類型),返回新數組| 設計考量 | 選擇 | 假設選 |
|---|---|---|
| 類型擴寬(子 List → 父數組) | ✅ 支援 | ❌ 編譯阻擋 |
| 運行時 ArrayStoreException | 仍可能拋出(JVM 檢查) | 同樣可能(數組協變) |
| 二進制兼容(JDK 1.4 → 5) | ✅ 擦除後簽名一致 | ⚠️ 橋接方法/序列化風險 |
<T> T[] toArray(T[] a) 是在「編譯期靈活性」「運行期安全性」「歷史兼容性」三角權衡下的最優解。理解其本質,才能在代碼審查中自信判斷正確性。