前言
在学习 Java 集合框架时,HashMap 的遍历是最基础但也最容易“知其然不知其所以然”的知识点。最近我在练习 HashMap 遍历时,写了两种常见的遍历方式,虽然都能跑通,但在查阅资料后才发现背后藏着不少性能陷阱和设计精髓。
本文以一段实际测试代码为切入点,记录我的学习过程、易错点总结以及对 entrySet() 底层原理的理解,希望能帮到同样在打基础的你。
我的测试代码
@Test
void forMap() {
HashMap<String, String> map = new HashMap<>();
map.put("1", "xiaoming");
map.put("2", "libai");
map.put("3", "dufu");
map.put("4", "zhangsan");
// ========== 方式一:keySet 遍历 ==========
Set<String> keySet = map.keySet();
System.out.println(keySet);
for (String key : keySet) {
// 每次循环都要额外调用 get()
System.out.println(map.get(key));
}
System.out.println("-".repeat(10));
// ========== 方式二:entrySet 遍历 ==========
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key=" + entry.getKey());
System.out.println("value=" + entry.getValue());
}
}
学习过程:从"能用"到"理解"
第一阶段:只会用 keySet
刚开始学的时候,我觉得 keySet() 很直观——先拿到所有 key,再通过 key 去查 value,逻辑清晰。但很快我就发现一个问题:如果 Map 很大,这种方式明显变慢了。
第二阶段:发现 entrySet 更高效

后来了解到 entrySet() 可以在一次迭代中同时拿到 key 和 value,不需要二次查找。改造代码后发现确实更简洁,但新的疑问来了:
entrySet()返回的到底是什么?为什么它能直接拿到 key 和 value?
第三阶段:深入理解"视图"概念
查阅源码后才恍然大悟:entrySet() 不是创建了一个新集合,而是返回了原 Map 的一个"实时视图"。它就像一张库存清单,本身不存数据,看到和操作的都是 Map 本身。这也解释了为什么通过 entry.setValue() 能直接修改原 Map。
⚠️ 核心易错点总结
易错点1:keySet 遍历中的 N+1 查询问题
// 错误示范:时间复杂度 O(n) 次哈希查找
for (String key : map.keySet()) {
String value = map.get(key); // 每次都重新计算 hash、定位桶、遍历链表
}
- 问题本质:
get()需要重新走一遍哈希定位流程 - 性能影响:当 Map 数据量大时,额外的 n 次哈希开销不可忽视
- 正确做法:需要同时访问 key 和 value 时,永远优先使用
entrySet()
易错点2:遍历时直接用 map.remove() 导致 ConcurrentModificationException
// 运行时异常!
for (Map.Entry<String, String> entry : map.entrySet()) {
if ("dufu".equals(entry.getValue())) {
map.remove(entry.getKey()); // 禁止!
}
}
- 原因:增强 for 底层是迭代器,直接调用 Map 的 remove 会修改
modCount,触发 fail-fast 机制 - 正确做法:使用迭代器的
remove()方法
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if ("dufu".equals(entry.getValue())) {
it.remove(); // 安全删除
}
}
易错点3:误以为 entrySet() 是独立副本
Set<Map.Entry<String, String>> entries = map.entrySet();
map.put("5", "lisi");
System.out.println(entries.size()); // 5!不是4!
- 认知纠正:
entrySet()是实时视图,不是快照。原 Map 变了,视图自动变;视图中删除元素,原 Map 也变 - 注意:视图不支持 add 操作,会抛出
UnsupportedOperationException
易错点4:泛型遗漏警告
// Raw type 警告
for (Map.Entry entry : map.entrySet()) { ... }
// 加上泛型
for (Map.Entry<String, String> entry : map.entrySet()) { ... }
不加泛型虽然能编译通过,但 getKey()/getValue() 返回的是 Object,后续使用需要强转,既不安全也不优雅。
一张图记住三种遍历场景
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 只需要 key | keySet() | 无需多余对象 |
| 只需要 value | values() | 返回 Collection,非 Set |
| 同时需要 key + value | entrySet() | 最佳实践,避免 N+1 |
| 遍历时需要删除元素 | Iterator.remove() | 唯一安全的删除方式 |
| JDK8+ 函数式风格 | forEach((k,v)->{}) | 内部已优化为 entrySet 遍历 |
底层原理速记(面试加分项)
Q:
entrySet()为什么能有迭代器效果?
- 增强 for → 编译器转为
Iterator entrySet()返回内部类EntrySet(继承AbstractSet)EntrySet.iterator()返回EntryIteratorEntryIterator继承HashIterator,通过 “扫描桶数组 + 遍历链表/红黑树” 直接访问 Node 节点- Node 本身实现了
Map.Entry接口 → 零拷贝、惰性遍历
总结
| 维度 | keySet | entrySet |
|---|---|---|
| 返回类型 | Set<K> | Set<Map.Entry<K,V>> |
| 获取 value | 需额外 get() | 直接 getValue() |
| 时间复杂度 | O(n × hash成本) | O(n) |
| 适用场景 | 仅用 key | 同时用 key + value |
| 本质 | Map 的 key 视图 | Map 的键值对视图 |
一句话口诀:
单用 key 走 keySet,key-value 一起拿就走 entrySet;遍历删除用迭代器,entrySet 是视图不是副本。

472

被折叠的 条评论
为什么被折叠?



