Java 遍历 Map 的五种方式与避坑指南

作者:热心市民鹿先生2026.11.07 02:30浏览量:0

简介:本文系统梳理 Java 中遍历 Map 的五种主流方式,并深入剖析迭代器使用中的三类高频异常:越界异常、重复调用 next()、并发修改异常。你将掌握每种遍历方式的适用场景、性能差异与线程安全注意事项,学会在遍历中安全删除元素的正确姿势,从而写出更稳健的集合操作代码。

Java 遍历 Map 的五种方式与避坑指南

在 Java 开发中,Map 是最常用的数据结构之一,用于存储键值对映射。无论是统计词频、缓存数据,还是构建配置表,遍历 Map 都是高频操作。看似简单的遍历,却隐藏着不少“坑”:越界异常、元素跳过、并发修改异常,稍不注意就会导致线上故障。

本文将系统梳理 Java 中遍历 Map 的五种主流方式,并针对迭代器使用中的典型错误做深入剖析,帮助你写出既高效又稳健的代码。

一、为什么遍历 Map 需要“讲究方式”

Map 本身不是 Collection 的子接口,它没有直接实现 Iterable,因此不能直接用增强 for 循环遍历。要遍历 Map,必须先获取它的某种“视图”:

  • keySet():所有键的集合
  • values():所有值的集合
  • entrySet():所有键值对(Map.Entry)的集合

不同的视图对应不同的遍历方式,性能与可读性也有差异。下面逐一展开。

二、五种遍历方式详解

1. 通过 keySet() 遍历键,再取值

这是最直观的方式:先拿到所有键,再通过 get(key) 获取值。

  1. Map<String, Integer> map = new HashMap<>();
  2. map.put("a", 1);
  3. map.put("b", 2);
  4. for (String key : map.keySet()) {
  5. Integer value = map.get(key);
  6. System.out.println(key + " = " + value);
  7. }

优点:代码简单,适合只需要键的场景。
缺点:如果同时需要键和值,每个键都要额外调用一次 get,在数据量大时性能损耗明显。因为 get 需要重新计算哈希并查找桶,相当于遍历了两遍。

适用场景:只关心键,或者 Map 规模很小。

2. 通过 values() 遍历值

如果只关心值,不需要键,直接遍历 values() 即可。

  1. for (Integer value : map.values()) {
  2. System.out.println(value);
  3. }

优点:语义清晰,性能好。
缺点:无法获取对应的键。

适用场景:只需要对所有值做统一处理,例如求和、过滤。

3. 通过 entrySet() 遍历键值对(推荐)

这是最常用、也是性能最好的方式。entrySet() 返回的是键值对的集合,遍历时直接拿到 Map.Entry,无需二次查找。

  1. for (Map.Entry<String, Integer> entry : map.entrySet()) {
  2. String key = entry.getKey();
  3. Integer value = entry.getValue();
  4. System.out.println(key + " = " + value);
  5. }

优点:一次遍历同时拿到键和值,效率高,可读性好。
缺点:代码略长于前两种。

适用场景:绝大多数需要同时使用键和值的场景,是首选方式。

4. 使用迭代器(Iterator)遍历

在 Java 8 之前,或者需要在遍历中删除元素时,迭代器是标准做法。

  1. Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
  2. while (it.hasNext()) {
  3. Map.Entry<String, Integer> entry = it.next();
  4. System.out.println(entry.getKey() + " = " + entry.getValue());
  5. }

优点:可以在遍历过程中安全地删除元素(使用 it.remove())。
缺点:代码相对繁琐。

适用场景:需要在遍历中删除元素的场景。

5. 使用 Lambda 表达式遍历(Java 8+)

Java 8 引入了 forEach 默认方法,可以用 Lambda 或方法引用简化遍历。

  1. map.forEach((key, value) -> System.out.println(key + " = " + value));

优点:代码极其简洁,可读性强。
缺点:不易在遍历中做复杂的控制流(如 break、continue);Lambda 中修改外部局部变量受限。

适用场景:对每个键值对做简单处理,无需中断遍历。

三、迭代器使用中的三大高频异常

无论使用哪种遍历方式,只要底层用到迭代器(增强 for 循环本质上也是迭代器),就必须注意以下三类问题。

1. 越界异常:NoSuchElementException

错误原因:迭代器的 next() 方法在内部会先检查是否还有元素。如果 hasNext() 已经返回 false,说明没有更多元素了,此时强行调用 next(),就会抛出 NoSuchElementException。

错误示例:

  1. Iterator<String> it = list.iterator();
  2. while (it.hasNext()) {
  3. it.next();
  4. }
  5. it.next(); // 越界,抛出 NoSuchElementException

规避方法:每次调用 next() 之前,必须先用 hasNext() 进行检查。养成“先问再取”的习惯,是使用迭代器的基本纪律。

2. 多次调用 next():元素跳过与越界

错误操作:在一个循环体内多次调用 it.next()。

  1. while (it.hasNext()) {
  2. System.out.println(it.next() + it.next());
  3. }

后果:

  • 每次循环消费两个元素,导致奇数个元素时最后一次循环只有一个元素可读,第二次 next() 抛越界异常;
  • 即使是偶数个元素,也会导致输出逻辑混乱,跳过部分元素的处理。

原理:next() 每调用一次,迭代器指针就向后移动一位。多次调用意味着指针一次前进多步。

规范:一次 hasNext() 对应一次 next()。如果需要暂存当前元素,应先赋值给局部变量,再基于该变量做多次使用。

  1. while (it.hasNext()) {
  2. String element = it.next();
  3. System.out.println(element + element);
  4. }

3. 并发修改异常:ConcurrentModificationException

现象:在迭代器遍历过程中,使用集合对象自身的方法(如 map.remove()、list.add())改变了集合的结构,程序抛出 ConcurrentModificationException。

原理:迭代器内部维护了一个“预期修改计数器”(modCount)。创建迭代器时,它记录了当前集合的修改次数。每次调用 next() 或 remove() 时,迭代器都会检查集合当前的修改次数是否与预期一致。如果集合自己改了长度,计数器对不上,迭代器就认为发生了并发修改,立即报错。

注意:这里的“并发”并不一定是多线程,单线程下在遍历时修改集合同样会触发。

错误示例:

  1. for (String key : map.keySet()) {
  2. if ("a".equals(key)) {
  3. map.remove(key); // 抛出 ConcurrentModificationException
  4. }
  5. }

解决方案:如果遍历时需要删除元素,必须使用迭代器自己的 remove() 方法。

  1. Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
  2. while (it.hasNext()) {
  3. Map.Entry<String, Integer> entry = it.next();
  4. if ("a".equals(entry.getKey())) {
  5. it.remove(); // 安全删除
  6. }
  7. }

it.remove() 会同步更新迭代器的预期计数器,因此不会报错。

补充说明:Java 8 起,Collection.removeIf() 提供了一种更简洁的安全删除方式:

  1. map.entrySet().removeIf(entry -> "a".equals(entry.getKey()));

其内部同样基于迭代器实现,语义更清晰。

四、多线程环境下的额外考量

上述讨论均基于单线程。如果多个线程同时读写同一个 Map,即使使用迭代器的 remove() 也无法保证安全。此时需要考虑:

  • 使用 ConcurrentHashMap,它支持并发读写,迭代器为弱一致性,不会抛 ConcurrentModificationException;
  • 或者使用外部同步机制(如 synchronized 块)包裹遍历与修改操作;
  • 或者采用“快照”思路,先复制一份集合再遍历。

选择哪种,取决于读写比例、一致性要求与性能预算。

五、实践建议汇总

场景 推荐方式
只需键 keySet() 增强 for
只需值 values() 增强 for
需要键和值 entrySet() 增强 for
遍历中删除元素 迭代器 + it.remove() 或 removeIf
简单处理,Java 8+ forEach + Lambda
多线程读写 ConcurrentHashMap 或外部同步

核心纪律:

  1. 先 hasNext(),再 next();
  2. 一次 hasNext() 对应一次 next();
  3. 遍历中删除只用迭代器的 remove();
  4. 多线程场景优先选用并发容器。

掌握这五种遍历方式与三类异常的本质,你就能在 Map 操作中游刃有余,写出既简洁又可靠的 Java 代码。