HashMap 是 Java 最高頻使用的集合,遍歷方式卻暗藏效能陷阱。本文對比七種遍歷寫法,從位元碼、JIT 內聯、記憶體分配、線程安全四個維度深度剖析,給出生產環境選型建議。
Map map = new HashMap<>();
map.put("a", 1); map.put("b", 2); map.put("c", 3);
// 1. EntrySet + for-each (推薦通用)
for (Map.Entry e : map.entrySet()) { ... }
// 2. KeySet + for-each + get (兩次查找)
for (String k : map.keySet()) { Integer v = map.get(k); ... }
// 3. Iterator + EntrySet (可刪除)
Iterator> it = map.entrySet().iterator();
while (it.hasNext()) { Map.Entry e = it.next(); if (...) it.remove(); }
// 4. forEach + Lambda (Java 8+)
map.forEach((k, v) -> { ... });
// 5. Stream API (函數式、可並行)
map.entrySet().stream().forEach(e -> { ... });
map.entrySet().parallelStream().forEach(e -> { ... }); // 並行需注意線程安全
// 6. Values 只遍歷值
for (Integer v : map.values()) { ... }
// 7. 傳統 for + EntrySet.toArray() (極少用)
Map.Entry[] entries = map.entrySet().toArray(new Map.Entry[0]);
for (Map.Entry e : entries) { ... }
| 方式 | 位元碼特徵 | 記憶體分配 | JIT 內聯 | 適用場景 |
|---|---|---|---|---|
| 1. EntrySet for-each | iterator() + hasNext/next | 每次迭代分配 Map.Entry 節點引用 (極小) | ✅ 極易內聯 | **通用首選**,可讀性最佳 |
| 2. KeySet + get | 兩次 getNode 查找 | 同 1,但 **雙倍哈希計算與桶定位** | ✅ | ❌ 避免,除非極簡單邏輯 |
| 3. Iterator 顯式 | 同 1,可呼叫 remove() | 同 1 | ✅ | 遍歷中需**安全刪除** |
| 4. forEach Lambda | forEachRemaining + Lambda 實例 | Lambda 實例捕獲變數可能分配 | ✅ (Java 8+) | 簡單副作用邏輯,代碼緊湊 |
| 5. Stream | 建立 Stream 管道、Spliterator | 額外 Stream/Spliterator 對象 | 部分內聯 | 需**鏈式轉換/過濾/聚合/並行** |
| 6. Values | 同 1,無 Key | 同 1 | ✅ | 只需值,不需 Key |
| 7. toArray() | 一次性複製整個 EntrySet 到陣列 | O(n) 額外陣列記憶體 | ✅ | 需**快照隔離並發修改** |
| 場景 | 問題 | 解決 |
|---|---|---|
| 單執行緒遍歷中修改 | ConcurrentModificationException (fail-fast) | 用 Iterator.remove() 或收集待刪 Key 後二次刪除 |
| 多執行緒並發讀寫 | 數據不一致、死循環 (JDK 7)、CME | ConcurrentHashMap (弱一致性迭代器)、Collections.synchronizedMap (鎖全圖) |
| 並行 Stream 遍歷共享可變狀態 | 競態條件、結果錯誤 | 無狀態 Lambda、ConcurrentHashMap、Collectors.toConcurrentMap |
// 過濾 + 轉換 + 收集
Map filtered = map.entrySet().stream()
.filter(e -> e.getValue() > 10)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// 分組統計
Map freq = map.values().stream()
.collect(Collectors.groupingBy(v -> v, Collectors.counting()));
// 並行聚合 (大數據量)
long sum = map.values().parallelStream().mapToInt(Integer::intValue).sum();
// forEachOrdered 保證順序 (LinkedHashMap)
map.forEachOrdered((k, v) -> System.out.println(k + "=" + v));
values() for-eachentrySet() for-each (方式 1)Iterator 顯式 (方式 3)parallelStream()toArray() (方式 7) 或 ConcurrentHashMapforEach Lambda (方式 4)keySet() + get() — 雙倍查找開銷,嚴禁熱路徑使用ConcurrentHashMap 或串行map.remove(key) — 必拋 CMEforEach 避免 Iterator 對象分配 (JIT 會標量替換)LinkedHashMap 保持插入/訪問順序,配合 forEachOrdered遍歷 HashMap 看似微末,實則折射出「迭代器模式 vs 函數式管道、快照隔離 vs 弱一致性、串行 vs 並行」的工程權衡。掌握七種方式的底層機制與適用邊界,能讓你在代碼審查時一眼識別效能隱患,在高併發場景下寫出正確且快的遍歷邏輯。