在 Java 接口設計中,直接暴露數組(如 String[] getNames())看似簡單高效,實則違反面向對象原則,帶來效能、型別安全、API 演進等多重隱患。本文從效能實測、型別系統、封裝性三個維度深度剖析,並給出最佳替代方案。
許多人認為數組是底層結構,直接返回應該最快。實測證明恰恰相反:
// 基準測試環境:JDK 17, n=1000, 10000 次迭代
// 返回 ArrayList 複本(紅柱) > 返回不可變列表(黃柱) > 數組 toArray(new String[0])(橙柱) > 數組 toArray(new String[size])(綠柱)
核心原因:
ArrayList.toArray(new String[0]) 使用無型別 toArray(),JVM 內部優化極佳new String[size] 反而觸發陣列填零、類型檢查額外開銷Collections.unmodifiableList() 為 O(1) 操作,零拷貝,效能最優Number[] numbers = new Integer[10];
numbers[0] = Long.valueOf(0); // 編譯通過,運行拋 ArrayStoreException
數組是協變的(Integer[] 是 Number[] 子類型),但泛型是不變的(List<Integer> 不是 List<Number> 子類型)。這導致:
Number[],調用者可傳入 Integer[],運行時注入 Long 導致崩潰new List<String>[] 編譯錯誤| 特性 | 數組 | 集合 |
|---|---|---|
| 有意義的 toString() | ❌ [Ljava.lang.String;@1a2b3c | ✅ [foo, bar] |
| 正確的 equals() | ❌ 引用比較 | ✅ 內容比較 |
| 豐富操作 API | ❌ 只能下標訪問 | ✅ 過濾、映射、分組、連接... |
| 線程安全選項 | ❌ 無 | ✅ Collections.synchronizedList 等 |
| 不可變視圖 | ❌ 無 | ✅ unmodifiableList、Guava ImmutableList |
public List<String> getNames() {
return new ArrayList<>(internalList); // O(n) 拷貝,調用者可安全修改
}
public List<String> getNames() {
return Collections.unmodifiableList(internalList); // O(1),零拷貝
}
// 注意:內部列表修改會反映到視圖,迭代時可能拋 ConcurrentModificationException
}
public List<String> getNames() {
return List.copyOf(internalList); // Java 10+,真正不可變,Null 安全
}
public Stream<String> getNames() {
return internalList.stream(); // 調用者決定收集方式、並行與否
}
ArrayList 內部 Object[])List<T>、Set<T>、Stream<T>Arrays.asList()、List.of()、Stream.of() 適配「數組與泛型無法良好共存。若發現自己在混用它們並獲得編譯錯誤或警告,首要反應應該是用 List 替換數組。」—— 第 29 條
數組是實現細節,不屬於接口契約。優秀的 Java API 設計:內部用數組追求極致效能,對外暴露集合提供豐富語義與型別安全。這不僅是風格問題,關係到系統的可維護性、可擴展性與運行時穩定性。