entrySet() 遍历键值对普遍比 keySet() + get() 更快,因其避免重复哈希定位和节点查找;keySet() 天然去重但需额外 get() 开销,且遍历时修改结构会触发 ConcurrentModificationException。

entrySet() 和 keySet() 到底哪个更快
直接说结论:entrySet() 遍历键值对时普遍比 keySet() + 重复 get() 查找快,尤其在 Map 较大或 get() 开销高(如 ConcurrentHashMap、自定义哈希逻辑)时更明显。这不是玄学,而是少了一次哈希定位和节点遍历。
原因很简单:entrySet() 返回的是底层已组装好的 Map.Entry 视图,迭代器直接访问节点;而 keySet() 迭代后每次调 map.get(key),等于重新走一遍哈希计算、桶定位、链表/红黑树查找 —— 白费一次 O(1)~O(log n) 的工作。
- HashMap 小数据量(entrySet() 更稳
- ConcurrentHashMap 中
get()是加锁操作,keySet()循环会放大锁竞争 - TreeMap 的
get()是 O(log n),叠加 N 次就是 O(N log N),而entrySet()迭代是 O(N)
什么时候必须用 keySet() 而不是 entrySet()
只读键、不关心值,或者后续逻辑强依赖 key 类型且需明确类型推导(比如泛型擦除导致 entrySet() 的 Entry<K,V> 在某些上下文里不好写)。
更现实的场景是:你要对 key 做筛选、去重、转集合,又不打算碰 value。这时用 keySet() 更直白,也避免创建无用的 Entry 对象(虽然开销极小)。
立即学习“Java免费学习笔记(深入)”;
- 例如过滤出所有字符串长度 > 5 的 key:
map.keySet().stream().filter(k -> k.length() > 5).collect(...) - 若混用
entrySet()再取e.getKey(),语义绕,编译器也无法帮你省掉Entry实例化 -
keySet()返回的是Set<K>,天然去重;entrySet()是Set<Entry<K,V>>,即使 key 相同(不可能)、value 不同,Entry 也不同
forEach + lambda 下的性能陷阱
写 map.forEach((k, v) -> {...}) 看似简洁,但这是 HashMap 等部分实现提供的快捷方法,底层仍基于 entrySet()。它没问题,但要注意:这个语法糖仅适用于 Map 接口的默认实现(JDK 8+),不适用于所有 Map 子类 —— 比如 EnumMap 支持,但某些第三方 Map(如 Eclipse Collections 的 MutableMap)可能不支持该方法,编译直接报错。
- 跨框架或不确定运行时类型时,优先用显式
for (Map.Entry<K,V> e : map.entrySet()) -
forEach是内部迭代,无法用break或continue控制流程;想提前退出就得抛异常或改用传统 for 循环 - lambda 捕获外部变量时,若变量非 final 或“事实 final”,容易引发意料外的闭包行为,尤其是多线程遍历时
别忽略迭代器的 fail-fast 行为
无论用 entrySet() 还是 keySet(),只要在遍历过程中被其他线程修改了 Map 结构(增删元素),就会触发 ConcurrentModificationException。这不是性能问题,而是并发安全的硬约束。
-
HashMap和TreeMap的迭代器都是 fail-fast,哪怕只是主线程自己在循环里调map.remove(key),也会炸 - 要边遍历边删,必须用迭代器自己的
remove()方法:it.remove(),而不是map.remove() - 真需要并发遍历,选
ConcurrentHashMap并用forEach()或newKeySet()等线程安全 API,别硬套普通 Map 的习惯
keySet() 当成“更轻量”的默认选择 —— 大多数需要键值配对的场景,entrySet() 才是直觉正确、性能合理、语义清晰的那条路。最容易被忽略的,其实是迭代过程中的结构性修改,以及不同 Map 实现对遍历 API 的兼容性差异。











