Java 集合框架是日常開發中使用最頻繁的 API 之一。正確選擇集合類型,理解其內部實現,對於編寫高效、正確的程式碼至關重要。
集合框架概覽#
graph TD
subgraph Collection["Collection 介面"]
L[List<br/>有序、可重複]
S[Set<br/>無序、不可重複]
Q[Queue/Deque]
end
L --> AL[ArrayList]
L --> LL[LinkedList]
L --> V["Vector<br/><i>(已過時)</i>"]
S --> HS[HashSet]
S --> LHS[LinkedHashSet]
S --> TS[TreeSet]
Q --> AD[ArrayDeque]
Q --> LL2[LinkedList]
Q --> PQ[PriorityQueue]
subgraph Map["Map 介面(鍵值對)"]
HM[HashMap]
LHM[LinkedHashMap]
TM[TreeMap]
HT["Hashtable<br/><i>(已過時)</i>"]
CHM[ConcurrentHashMap]
end
style Collection fill:#e3f2fd
style Map fill:#fff3e0
style V fill:#ffcdd2
style HT fill:#ffcdd2
Map不是Collection的子類型,但通常被視為集合框架的一部分。
List 實現對比#
ArrayList vs LinkedList vs Vector#
| 特性 | ArrayList | LinkedList | Vector |
|---|---|---|---|
| 底層結構 | 動態陣列 | 雙向鏈結串列 | 動態陣列 |
| 線程安全 | 否 | 否 | 是(synchronized) |
| 隨機訪問 | O(1) | O(n) | O(1) |
| 頭部插入 | O(n) | O(1) | O(n) |
| 尾部插入 | 均攤 O(1) | O(1) | 均攤 O(1) |
| 擴容策略 | 增加 50% | 無需擴容 | 增加 100% |
Vector 是 Java 早期的線程安全實現,同步開銷大。現代開發中應該使用
Collections.synchronizedList()或CopyOnWriteArrayList。
選擇策略#
// 大部分場景:ArrayList
List<String> list = new ArrayList<>();
// 頻繁在頭部/中間插入刪除:LinkedList
Deque<String> deque = new LinkedList<>();
// 需要線程安全且讀多寫少:CopyOnWriteArrayList
List<String> safeList = new CopyOnWriteArrayList<>();LinkedList 遍歷的效能陷阱
錯誤做法:使用 for 循環遍歷 LinkedList
// 時間複雜度 O(n²)!每次 get(i) 都要從頭遍歷
for (int i = 0; i < linkedList.size(); i++) {
linkedList.get(i);
}正確做法:使用迭代器或增強 for 循環
// 時間複雜度 O(n)
for (String item : linkedList) {
// 處理 item
}
// 或使用迭代器
Iterator<String> it = linkedList.iterator();
while (it.hasNext()) {
it.next();
}Map 實現對比#
HashMap 內部結構#
Java 8 的 HashMap 採用陣列 + 鏈結串列 + 紅黑樹的混合結構:
graph LR
subgraph HashMap["HashMap 內部結構"]
B0["bucket[0]"] --> N1[Node] --> N2[Node] --> N3[...]
B1["bucket[1]"] --> NULL[null]
B2["bucket[2]"] --> T1["TreeNode<br/>(紅黑樹)"]
B3["..."]
BN["bucket[n-1]"] --> NN[Node]
end
style T1 fill:#ffccbc
style NULL fill:#f5f5f5當單個桶中的鏈結串列長度超過 8 且陣列容量達到 64 時,鏈結串列會轉換為紅黑樹(樹化),將查詢複雜度從 O(n) 降為 O(log n)。
HashMap vs Hashtable vs TreeMap#
| 特性 | HashMap | Hashtable | TreeMap |
|---|---|---|---|
| 線程安全 | 否 | 是 | 否 |
| null 鍵值 | 允許一個 null 鍵 | 不允許 | 不允許 null 鍵 |
| 順序 | 無序 | 無序 | 按鍵排序 |
| 時間複雜度 | O(1) | O(1) | O(log n) |
Hashtable 已過時,需要線程安全的 Map 應使用
ConcurrentHashMap。
HashMap 的關鍵參數#
// 預設初始容量:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 預設負載因子:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 樹化閾值:8
static final int TREEIFY_THRESHOLD = 8;
// 反樹化閾值:6
static final int UNTREEIFY_THRESHOLD = 6;如果事先知道資料量,建議指定初始容量以避免擴容:
// 預計存放 100 個元素,考慮負載因子 Map<String, Object> map = new HashMap<>(128);
HashMap 的 hash 演算法最佳化
HashMap 不直接使用 hashCode(),而是進行擾動處理:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}這樣做是為了讓高位也參與運算,減少碰撞。因為定位桶的運算是:
index = (n - 1) & hash當 n 較小時,只有低位參與運算,高位資訊會被忽略。
Set 實現對比#
| 實現 | 特點 | 時間複雜度 |
|---|---|---|
| HashSet | 無序,基於 HashMap | O(1) |
| LinkedHashSet | 保持插入順序 | O(1) |
| TreeSet | 自然排序或自定義排序 | O(log n) |
// HashSet 實際上是 HashMap 的馬甲
public class HashSet<E> {
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
}TreeSet 底層使用 TreeMap 實現,HashSet 底層使用 HashMap 實現。它們只使用了 Map 的 key,value 都是同一個 dummy 物件。
線程安全的集合#
Collections.synchronized 系列#
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Object> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());synchronized 包裝器採用粗粒度鎖,效能較差。遍歷時仍需手動同步:
synchronized (syncList) { for (String s : syncList) { // 處理 } }
ConcurrentHashMap#
Java 8 的 ConcurrentHashMap 採用 CAS + synchronized 實現細粒度鎖:
// 初始化時使用 CAS
private final Node<K,V>[] initTable() {
while ((tab = table) == null || tab.length == 0) {
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 初始化
}
}
}
// 插入時只鎖定單個桶
synchronized (f) {
// 插入操作
}Java 7 vs Java 8 的 ConcurrentHashMap
Java 7:分段鎖(Segment)
- 將整個 Map 分成 16 個 Segment
- 每個 Segment 獨立加鎖
- 理論最大並行度為 16
Java 8:CAS + synchronized
- 取消 Segment,直接鎖定桶
- 粒度更細,並行度更高
- 引入紅黑樹最佳化
ConcurrentHashMap 的常見誤用
// 錯誤:非原子操作 if (!map.containsKey(key)) { map.put(key, value); } // 正確:使用原子方法 map.putIfAbsent(key, value); map.computeIfAbsent(key, k -> createValue());
CopyOnWriteArrayList#
適用於讀多寫少的場景:
// 每次寫操作都會複製整個陣列
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
Object[] newElements = Arrays.copyOf(es, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}CopyOnWriteArrayList 適合作為事件監聽器列表、黑白名單等讀操作遠多於寫操作的場景。
Java 8+ 集合增強#
Stream API#
List<String> filtered = list.stream()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());工廠方法(Java 9+)#
// 不可變集合
List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
List.of()等方法創建的集合是不可變的,不支援 add、remove 操作。
實踐建議#
- 預設選擇 ArrayList 和 HashMap:大多數場景的最佳選擇
- 需要排序用 TreeMap/TreeSet:自動維護有序狀態
- 需要保持插入順序用 LinkedHashMap/LinkedHashSet
- 並行場景用 ConcurrentHashMap:避免使用 Hashtable
- 讀多寫少用 CopyOnWriteArrayList:注意寫操作的開銷
- 指定初始容量:避免頻繁擴容帶來的效能損耗