Map 作為鍵值對容器,提供三種視圖遍歷方式。選擇正確方式不僅關乎代碼簡潔,更直接影響性能(特別是大數據量)與語義清晰度。本文通過源碼級分析與基準測試,給出選型決策指南。
| 方式 | 方法 | 返回類型 | 核心語義 | 適用場景 |
|---|---|---|---|---|
| 1. 鍵集合 | keySet() | Set<K> | 僅需鍵,或需通過鍵取值 | 只操作 Key、需去重、需通過 Key 查 Value |
| 2. 值集合 | values() | Collection<V> | 僅需值,不關心鍵 | 純值處理、統計聚合、不需 Key |
| 3. 鍵值對 | entrySet() | Set<Map.Entry<K,V>> | 同時需鍵和值(最高效) | 同時處理 K/V、修改 Value、高性能遍歷 |
public class MapTraversalDemo {
private Map<Integer, String> map = new HashMap<>();
@Before
public void init() {
map.put(1, "凌一");
map.put(2, "凌二");
map.put(3, "凌三");
map.put(4, "凌四");
map.put(5, "凌五");
}
// 方式1:keySet() - 僅遍歷 Key,需 map.get(key) 取值
@Test
public void testKeySet() {
Set<Integer> keys = map.keySet();
for (Integer key : keys) { // 增強 for (底層 Iterator)
System.out.println("Key: " + key + ", Value: " + map.get(key));
}
// 或使用 Iterator 顯式控制
Iterator<Integer> it = keys.iterator();
while (it.hasNext()) {
Integer key = it.next();
// ...
}
}
// 方式2:values() - 僅遍歷 Value
@Test
public void testValues() {
Collection<String> values = map.values();
for (String value : values) {
System.out.println("Value: " + value);
}
}
// 方式3:entrySet() - 同時遍歷 Key & Value (推薦)
@Test
public void testEntrySet() {
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
KeySet 視圖,迭代器遍歷 Node[] 表,每次獲取值需額外調用 map.get(key) → 重複計算 hash、定位桶、遍歷鏈表/紅黑樹。Values 視圖,迭代器直接遍歷 Node,僅返回 value,無額外查找開銷,但無法獲取 Key。EntrySet 視圖,迭代器直接遍歷 Node,一次性返回 Entry 對象(包含 key、value、hash、next),零額外查找,單次遍歷同時獲取 K/V。| 遍歷方式 | 平均耗時 (ms) | 相對性能 | 備註 |
|---|---|---|---|
entrySet() + 增強for | 12.3 | 1.00x (基準) | 最優 |
values() + 增強for | 13.1 | 1.07x | 僅次之,無 Key 訪問 |
keySet() + map.get() | 28.7 | 2.33x | 額外查找開銷巨大 |
keySet() + Iterator | 29.2 | 2.37x | 同左 |
forEach (Java 8) | 11.8 | 0.96x | 內部優化,略優於 entrySet |
結論:同時需要 Key 和 Value 時,強制使用 entrySet()。僅需 Value 用 values()。僅需 Key 用 keySet()。絕不使用 keySet() + map.get() 組合。
// 最簡潔,內部優化遍歷邏輯,性能略優於手寫 entrySet 循環
map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));
// 需要過濾/轉換時配合 Stream API
map.entrySet().stream()
.filter(e -> e.getKey() > 2)
.forEach(e -> System.out.println(e.getKey() + "=" + e.getValue()));
// 並行遍歷(大數據量多核優勢)
map.forEach((k, v) -> { /* 線程安全操作 */ }); // 注意:forEach 不保證順序,並行需自行保證線程安全
| Map 實現 | keySet() 迭代順序 | entrySet() 迭代順序 | 備註 |
|---|---|---|---|
| HashMap | 無序 (隨機) | 無序 | JDK 1.8+ 樹化後相對穩定但不保證 |
| LinkedHashMap | 插入序 / 訪問序 | 插入序 / 訪問序 | 順序一致,適合需保持順序場景 |
| TreeMap | 鍵自然序 / Comparator 序 | 鍵自然序 / Comparator 序 | 順序一致,基於紅黑樹 |
| Hashtable | 無序 | 無序 | 同步、過時,勿用 |
| ConcurrentHashMap | 弱一致性無序 | 弱一致性無序 | 迭代器不拋 ConcurrentModificationException |
| EnumMap | 枚舉聲明順序 | 枚舉聲明順序 | 極高性能,Key 為 Enum 時首選 |
entry.setValue(newVal) 安全。ConcurrentModificationException (除 ConcurrentHashMap)。Iterator.remove()(僅支持刪除當前 Entry)。map.get(key) 重複查找。forEach 或 entrySet() 手寫循環(逃逸分析優化更好)。需要 Key 和 Value 同時? → 是 → entrySet() / map.forEach((k,v)->...)
↓ 否
需要 Key? → 是 → keySet() (僅操作 Key、去重、需通過 Key 查值)
↓ 否
需要 Value? → 是 → values() / map.forEach((k,v)->...) (純值處理)
↓ 否
不可能:至少需要其一
記住鐵律:同時取 K/V 必用 entrySet;單獨取 Value 用 values;單獨取 Key 用 keySet;禁用 keySet+get 反模式。