引言:為什麼要關心遍歷方式?
Map 遍歷是高頻操作,選錯方式可能導致 10x 性能差距。本文基於 JMH 基準測試,系統對比 keySet、entrySet、values 在 HashMap 與 TreeMap 下的三種場景:遍歷 K+V、僅遍歷 K、僅遍歷 V。
測試環境與數據構造
- JDK 17, Intel i7-12700H, 16GB RAM
- 數據量:1,000,000 鍵值對
- Key 類型:
String(數字字符串 "1"..."1000000" vs 稀疏 "50,100,150...")
- Value 類型:固定字符串
"value"
核心結論速覽表
| Map 類型 | 場景 | 最優方式 | 次優 | 最差 | 核心原因 |
| HashMap | K+V | entrySet ≈ keySet | - | - | HashMap get() 極快(O(1)),entrySet 多創建 Entry 對象抵消優勢 |
| 僅 K | keySet | entrySet | - | entrySet 多讀 Value 浪費 |
| 僅 V | values | entrySet | keySet | keySet 需額外 get() |
| TreeMap | K+V | entrySet ≫ keySet | - | keySet | TreeMap get() 為 O(logN) 紅黑樹查找,entrySet 迭代器直接遍歷節點 |
| 僅 K | keySet | entrySet | - | 同 HashMap |
| 僅 V | values | entrySet | keySet | keySet 需 get(),TreeMap 極慢 |
詳細數據(單位:ms/op,100 萬數據,平均 5 輪)
HashMap - 密集 Key ("1"..."1000000")
| 方式 | K+V | 僅 K | 僅 V |
| keySet (for-each) | 38 | 26 | 37 |
| entrySet (for-each) | 43 | 34 | 32 |
| values (for-each) | - | - | 26 |
TreeMap - 密集 Key
| 方式 | K+V | 僅 K | 僅 V |
| keySet (for-each) | 430 | 49 | 432 |
| entrySet (for-each) | 70 | 63 | 61 |
| values (for-each) | - | - | 45 |
原理深度解析
HashMap:entrySet 為何不穩定佔優?
entrySet 迭代器直接遍歷 Node[] table 鏈表/紅黑樹,每次返回 Map.Entry 對象(新建或復用);
keySet 迭代器同樣遍歷底層數組,但僅返回 Key;後續 map.get(key) 需重新計算 hash、定位桶、遍歷鏈表比較 equals;
- 關鍵點:HashMap
get() 極快(納秒級),但 entrySet 每次迭代分配/復用 Entry 對象也有開銷。兩者在現代 JVM(逃逸分析、標量替換)下往往持平。
TreeMap:entrySet 完勝的鐵律
- TreeMap 底層紅黑樹,
get(key) 需從根節點向下比較 O(logN),100 萬數據約 20 次比較;
keySet 遍歷 + get() = O(N logN);
entrySet 迭代器通過 nextEntry() 直接走樹的中序後繼節點,純指針跳轉 O(1) 均攤,總體 O(N);
- 因此 TreeMap 任何涉及 Value 的遍歷必須用 entrySet/values,keySet 只適合純 Key 遍歷。
最佳實踐代碼模板
// 1. 同時需要 Key 和 Value → entrySet (通用最優,TreeMap 必須)
for (Map.Entry entry : map.entrySet()) {
K k = entry.getKey();
V v = entry.getValue();
// process
}
// 2. 僅需要 Key → keySet
for (K k : map.keySet()) { /* process */ }
// 3. 僅需要 Value → values
for (V v : map.values()) { /* process */ }
// 4. Java 8+ Stream 風格(性能略低於 for-each,但可並行)
map.forEach((k, v) -> { /* process */ }); // K+V
map.keySet().forEach(k -> { /* process */ }); // K
map.values().forEach(v -> { /* process */ }); // V
map.entrySet().parallelStream().forEach(e -> { }); // 並行遍歷大數據
避坑指南
- 不要在遍歷中修改 Map:拋
ConcurrentModificationException。需修改請用 ConcurrentHashMap 或收集待刪 Key 後二次處理;
- LinkedHashMap 遍歷順序 = 插入順序/訪問順序,entrySet/keySet/values 均保持順序;
- EnumMap/IdentityHashMap/WeakHashMap 同理:涉及 Value 用 entrySet/values;
- 微優化:極熱路徑可將
entrySet() 結果賦值給局部變量,避免重複調用(JIT 通常會內聯消除)。
總結
HashMap:K+V 用 entrySet 或 keySet 差異微小,僅 K 用 keySet,僅 V 用 values。
TreeMap:凡涉及 Value 必須用 entrySet 或 values,keySet 僅限純 Key 遍歷。
記住口訣:「紅黑樹見值用 entry,哈希表隨意;只取鍵用 keySet,只取值用 values。」