數組(T[])與集合(List<T>)是 Java 中兩種最基礎的數據結構。數組定長、支援基本類型、效能高;List 變長、泛型豐富、API 強大。實際開發中頻繁在兩者間轉換,但 toArray() 重載陷阱、Arrays.asList() 固定長度限制、基本類型數組無法直接轉換等問題,常導致運行時異常或邏輯 Bug。本文系統梳理轉換姿勢、核心原理、常見坑與生產級工具方法。
List list = Arrays.asList("a", "b", "c");
Object[] objArr = list.toArray(); // 返回 Object[],不可直接轉 String[]
// String[] strArr = (String[]) objArr; // ClassCastException!
原因:無參版本返回 Object[],運行時類型擦除導致無法強轉。
// 方式 A:精確大小數組(推薦,零拷貝)
String[] arr = list.toArray(new String[0]); // JDK 6+ 優化:大小為 0 也會新建合適大小數組
// 方式 B:預分配大小數組(舊版本優化)
String[] arr2 = list.toArray(new String[list.size()]);
原理(ArrayList 源碼):
public T[] toArray(T[] a) {
if (a.length < size)
return (T[]) Arrays.copyOf(elementData, size, a.getClass()); // 新建同類型數組
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null; // 多餘位置置 null
return a;
}
new String[0] 觸發第二分支,JVM 逃逸分析可優化分配,現代 JDK 推薦此寫法。List intList = Arrays.asList(1, 2, 3);
// int[] primitiveArr = intList.toArray(new int[0]); // 編譯錯誤:int[] 非 Object 子類
// 必須手動轉換
int[] arr = intList.stream().mapToInt(Integer::intValue).toArray();
int[] intArr = {1, 2, 3};
List list1 = Arrays.asList(intArr); // 推斷類型 List,單個元素!
System.out.println(list1.size()); // 1
System.out.println(list1.get(0) == intArr); // true
String[] strArr = {"a", "b", "c"};
List list2 = Arrays.asList(strArr); // 推斷 List,正常展開
System.out.println(list2.size()); // 3
原因:asList(T... a) 參數是泛型可變參數。int[] 是物件,匹配 T = int[];String[] 匹配 T = String(可變參數展開)。
List list = Arrays.asList("a", "b");
list.add("c"); // UnsupportedOperationException
list.remove(0); // UnsupportedOperationException
list.set(0, "x"); // ✅ 允許:修改現有元素
list.clear(); // UnsupportedOperationException
原因:返回的是 Arrays.ArrayList(內部類),繼承 AbstractList,未重寫 add/remove/clear,拋出 UnsupportedOperationException。
String[] arr = {"a", "b"};
List list = Arrays.asList(arr);
list.set(0, "x");
System.out.println(arr[0]); // "x" —— 數組被修改!
arr[1] = "y";
System.out.println(list.get(1)); // "y" —— List 看到變化
本質:Arrays.ArrayList 內部直接持有傳入數組引用 private final E[] a,無拷貝。
public static List toMutableList(T[] array) {
if (array == null) return Collections.emptyList();
return new ArrayList<>(Arrays.asList(array)); // 拷貝構造
}
// 基本類型專用
public static List toMutableList(int[] array) {
if (array == null) return Collections.emptyList();
return Arrays.stream(array).boxed().collect(Collectors.toList());
}
public static T[] toArray(List list, Class clazz) {
if (list == null || list.isEmpty()) {
return (T[]) Array.newInstance(clazz, 0); // 返回空數組而非 null
}
return list.toArray((T[]) Array.newInstance(clazz, list.size()));
}
// 基本類型
public static int[] toPrimitiveArray(List list) {
if (list == null) return new int[0];
return list.stream().mapToInt(Integer::intValue).toArray();
}
// List -> Array
String[] arr = list.toArray(String[]::new); // 方法引用,簡潔高效
// Array -> List (可變)
List list = Arrays.stream(array).collect(Collectors.toList());
// 基本類型
int[] intArr = list.stream().mapToInt(Integer::intValue).toArray();
List intList = Arrays.stream(intArr).boxed().collect(Collectors.toList());
| 場景 | 推薦方式 | 時間複雜度 | 備註 |
|---|---|---|---|
| List → 數組 (引用類型) | list.toArray(new T[0]) | O(n) | JDK 6+ 優化,零反射開銷 |
| 數組 → 可變 List | new ArrayList<>(Arrays.asList(arr)) | O(n) | 雙重拷貝,但安全 |
| 數組 → 不可變視圖 (僅讀、極大數組) | Arrays.asList(arr) | O(1) | 零拷貝,共享底層數組,謹慎使用 |
| 基本類型數組 ↔ List | Stream API boxed()/mapToInt() | O(n) | 自動裝箱/拆箱開銷,無替代方案 |
| 高頻熱點路徑 | 手寫循環拷貝 / System.arraycopy | O(n) | 避免 Stream/Lambda 開銷,JIT 內聯最佳 |
list.toArray(new String[list.size()]) vs new String[0] 哪個快?new String[0] 更優。逃逸分析可消除數組分配,直接在堆上分配精確大小。舊版本預分配大小避免 Arrays.copyOf 擴容拷貝。Arrays.asList(1,2,3) 編譯通過但 Arrays.asList(intArr) 只有一個元素?1,2,3 裝箱為 Integer,T=Integer;int[] 是物件,T=int[]。Arrays.asList 返回的固定長度列表?list.getClass().getName().contains("Arrays$ArrayList") 或嘗試 list.add(x) 捕獲異常(不推薦)。Collections.emptyList() vs new ArrayList<>() 區別?Collections.emptyList()。數組與 List 轉換的核心難點在於:類型擦除導致的運行時類型缺失、基本類型與泛型的不兼容、固定長度視圖與可變副本的語義差異。掌握 toArray(T[]) 重載機制、Arrays.asList 返回類型特性、Stream API 統一轉換模式,並封裝空值安全、基本類型友好的工具方法,即可在業務代碼中游刃有餘。記住:**轉換即拷貝,拷貝即成本**——高頻熱點路徑請手寫優化,常規業務用標準庫保證可讀性。