简介:本文系统梳理 Java 中遍历 Map 的五种主流方式,并深入剖析迭代器使用中的三类高频异常:越界异常、重复调用 next()、并发修改异常。你将掌握每种遍历方式的适用场景、性能差异与线程安全注意事项,学会在遍历中安全删除元素的正确姿势,从而写出更稳健的集合操作代码。
在 Java 开发中,Map 是最常用的数据结构之一,用于存储键值对映射。无论是统计词频、缓存数据,还是构建配置表,遍历 Map 都是高频操作。看似简单的遍历,却隐藏着不少“坑”:越界异常、元素跳过、并发修改异常,稍不注意就会导致线上故障。
本文将系统梳理 Java 中遍历 Map 的五种主流方式,并针对迭代器使用中的典型错误做深入剖析,帮助你写出既高效又稳健的代码。
Map 本身不是 Collection 的子接口,它没有直接实现 Iterable,因此不能直接用增强 for 循环遍历。要遍历 Map,必须先获取它的某种“视图”:
keySet():所有键的集合values():所有值的集合entrySet():所有键值对(Map.Entry)的集合不同的视图对应不同的遍历方式,性能与可读性也有差异。下面逐一展开。
这是最直观的方式:先拿到所有键,再通过 get(key) 获取值。
Map<String, Integer> map = new HashMap<>();map.put("a", 1);map.put("b", 2);for (String key : map.keySet()) {Integer value = map.get(key);System.out.println(key + " = " + value);}
优点:代码简单,适合只需要键的场景。
缺点:如果同时需要键和值,每个键都要额外调用一次 get,在数据量大时性能损耗明显。因为 get 需要重新计算哈希并查找桶,相当于遍历了两遍。
适用场景:只关心键,或者 Map 规模很小。
如果只关心值,不需要键,直接遍历 values() 即可。
for (Integer value : map.values()) {System.out.println(value);}
优点:语义清晰,性能好。
缺点:无法获取对应的键。
适用场景:只需要对所有值做统一处理,例如求和、过滤。
这是最常用、也是性能最好的方式。entrySet() 返回的是键值对的集合,遍历时直接拿到 Map.Entry,无需二次查找。
for (Map.Entry<String, Integer> entry : map.entrySet()) {String key = entry.getKey();Integer value = entry.getValue();System.out.println(key + " = " + value);}
优点:一次遍历同时拿到键和值,效率高,可读性好。
缺点:代码略长于前两种。
适用场景:绝大多数需要同时使用键和值的场景,是首选方式。
在 Java 8 之前,或者需要在遍历中删除元素时,迭代器是标准做法。
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();while (it.hasNext()) {Map.Entry<String, Integer> entry = it.next();System.out.println(entry.getKey() + " = " + entry.getValue());}
优点:可以在遍历过程中安全地删除元素(使用 it.remove())。
缺点:代码相对繁琐。
适用场景:需要在遍历中删除元素的场景。
Java 8 引入了 forEach 默认方法,可以用 Lambda 或方法引用简化遍历。
map.forEach((key, value) -> System.out.println(key + " = " + value));
优点:代码极其简洁,可读性强。
缺点:不易在遍历中做复杂的控制流(如 break、continue);Lambda 中修改外部局部变量受限。
适用场景:对每个键值对做简单处理,无需中断遍历。
无论使用哪种遍历方式,只要底层用到迭代器(增强 for 循环本质上也是迭代器),就必须注意以下三类问题。
错误原因:迭代器的 next() 方法在内部会先检查是否还有元素。如果 hasNext() 已经返回 false,说明没有更多元素了,此时强行调用 next(),就会抛出 NoSuchElementException。
错误示例:
Iterator<String> it = list.iterator();while (it.hasNext()) {it.next();}it.next(); // 越界,抛出 NoSuchElementException
规避方法:每次调用 next() 之前,必须先用 hasNext() 进行检查。养成“先问再取”的习惯,是使用迭代器的基本纪律。
错误操作:在一个循环体内多次调用 it.next()。
while (it.hasNext()) {System.out.println(it.next() + it.next());}
后果:
next() 抛越界异常;原理:next() 每调用一次,迭代器指针就向后移动一位。多次调用意味着指针一次前进多步。
规范:一次 hasNext() 对应一次 next()。如果需要暂存当前元素,应先赋值给局部变量,再基于该变量做多次使用。
while (it.hasNext()) {String element = it.next();System.out.println(element + element);}
现象:在迭代器遍历过程中,使用集合对象自身的方法(如 map.remove()、list.add())改变了集合的结构,程序抛出 ConcurrentModificationException。
原理:迭代器内部维护了一个“预期修改计数器”(modCount)。创建迭代器时,它记录了当前集合的修改次数。每次调用 next() 或 remove() 时,迭代器都会检查集合当前的修改次数是否与预期一致。如果集合自己改了长度,计数器对不上,迭代器就认为发生了并发修改,立即报错。
注意:这里的“并发”并不一定是多线程,单线程下在遍历时修改集合同样会触发。
错误示例:
for (String key : map.keySet()) {if ("a".equals(key)) {map.remove(key); // 抛出 ConcurrentModificationException}}
解决方案:如果遍历时需要删除元素,必须使用迭代器自己的 remove() 方法。
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();while (it.hasNext()) {Map.Entry<String, Integer> entry = it.next();if ("a".equals(entry.getKey())) {it.remove(); // 安全删除}}
it.remove() 会同步更新迭代器的预期计数器,因此不会报错。
补充说明:Java 8 起,Collection.removeIf() 提供了一种更简洁的安全删除方式:
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 或外部同步 |
核心纪律:
hasNext(),再 next();hasNext() 对应一次 next();remove();掌握这五种遍历方式与三类异常的本质,你就能在 Map 操作中游刃有余,写出既简洁又可靠的 Java 代码。