keySet()适合只读key场景,轻量且内存友好,但循环中不可直接remove(key),须用Iterator.remove();entrySet()最通用,性能稳、代码清,支持setValue但不可改key;forEach()和Stream各有适用边界,需规避并发与短路陷阱。

用 keySet() 遍历只读场景,但别在循环里删 key
当你只需要 key、或者能通过 map.get(key) 拿 value 时,keySet() 最轻量。它返回的是底层 HashMap 的视图集合,不复制数据,内存友好。
常见错误是边遍历边调用 map.remove(key) —— 这会触发 ConcurrentModificationException,哪怕单线程也一样。Java 不允许通过非迭代器方式修改正在被迭代的集合。
- 安全删除:用
Iterator的remove()方法,或改用entrySet()+Iterator - 性能注意:如果频繁调用
map.get(key),且 map 很大,哈希查找开销叠加后可能不如直接遍历entrySet() - 适用场景:日志打印 key、权限校验(只看 key 是否存在)、批量初始化(key 已知,value 待设)
entrySet() 是最通用的遍历方式,90% 场景该选它
entrySet() 返回 Map.Entry 对象集合,每个对象同时持有 key 和 value 引用。相比 keySet() + 多次 get(),它避免了重复哈希定位,性能更稳,代码也更清晰。
注意:Map.Entry 是一个接口,不同实现类行为略有差异。比如 TreeMap 的 entrySet() 迭代顺序是 key 的自然序或自定义序;而 LinkedHashMap 保持插入顺序。
立即学习“Java免费学习笔记(深入)”;
- 修改 value:可直接调用
entry.setValue(newVal),这是合法且高效的(底层 map 结构同步更新) - 修改 key:不行。
entry.getKey()返回的是只读引用,强行 cast 或反射改 key 会导致 map 内部状态错乱 - 避免装箱陷阱:遍历
Map<integer string></integer>时,不要在循环内反复写entry.getKey().intValue(),提前存局部变量
Java 8 的 forEach() + Lambda 看似简洁,但有隐性限制
map.forEach((k, v) -> { ... }) 写起来短,但它本质是语法糖,底层仍走 entrySet() 迭代。问题在于:Lambda 里无法 break 或 continue,也没法抛检查异常(除非包装成 RuntimeException)。
更关键的是,它不支持并发安全操作。如果你在多线程环境用 ConcurrentHashMap,forEach() 保证不会抛 ConcurrentModificationException,但不保证遍历期间看到的是“某一个时间点”的快照——它可能跳过刚插入的 entry,也可能重复看到被移除又重建的 key。
- 适合场景:纯副作用操作,如打日志、发通知、写入外部缓存(不依赖遍历完整性)
- 不适合场景:需要提前退出(如查找匹配项)、需处理
IOException、要求强一致性遍历结果 - 替代方案:明确用
for (Map.Entry<K,V> e : map.entrySet()),控制力更强
Stream API 遍历灵活但别滥用,尤其注意短路操作失效
map.entrySet().stream()... 提供 filter/map/collect 等链式能力,适合数据转换类逻辑。但它默认是 延迟求值,且 findFirst()、anyMatch() 这类短路操作,在 ConcurrentHashMap 上可能不真正“短路”——因为 stream 的 spliterator 实现为分段遍历,即使找到第一个匹配项,其他线程段仍可能继续执行。
另一个坑是:Collectors.toMap() 在 key 冲突时默认抛 IllegalStateException,而原生 map 的 put() 是覆盖。这点容易在重构时漏掉。
- 必须用 Stream:需要并行处理(
parallelStream())、复杂条件组合、中间态聚合(如按 value 分组统计) - 慎用 Stream:简单遍历、对性能敏感(stream 创建开销 > for 循环)、需精确控制异常流
- 兼容性提醒:Android 旧版本(API Map 的
forEach()和stream(),得降级









