同一件事,好几种工具
你已经把一些数据放进了 ArrayList、HashSet 或 HashMap,现在想访问其中的每个元素。Java 提供了几种做法,选哪一种取决于:你是否需要索引,是否需要在循环中途删除元素,以及你更喜欢方法式还是循环式的语法。
好消息是:每个 Collection(列表、集合、队列)都支持同样的增强 for 循环,学会它之后,你就能用同一种方式遍历所有集合。
冒号可以读作“在……之中”:“对 langs 中的每个 lang”。你完全不碰索引,所以也就不会出错。
for-each 循环
对于单纯的“对每个元素做点事”,增强 for 是默认选择。它读起来清晰,在各种集合类型上的用法完全相同。下面用的是 HashSet,它根本没有索引:
关于 HashSet 要记住一点:它没有确定的顺序,所以元素可能以任意顺序打印出来。不过无论如何,for-each 都会把每个元素恰好访问一次。
需要索引的时候
for-each 给你元素,但不给你它的位置。如果你确实需要索引,比如给行编号,或者查看相邻的元素,就用带 size() 和 get(i) 的计数循环。这适用于 List,因为它是按位置排列的;Set 和 Map 没有索引,所以这种写法不适用于它们。
不要只是出于习惯就用这种写法。如果 i 除了 get(i) 之外没有别的用处,for-each 版本更短,也更不容易出错。
遍历 Map
Map 不是 Collection,所以不能直接用 for-each 遍历它,而是要遍历它的三个视图之一。最常用的是 entrySet(),它会把每个键值对一起交给你:
如果只需要键,就遍历 ages.keySet();如果只需要值,就遍历 ages.values()。键和值都需要时,优先用 entrySet():遍历键再在循环里调用 ages.get(key),会在每一轮无谓地多查找一次。
Iterator
for-each 其实是 Iterator 的语法糖。Iterator 是一个对象,通过 hasNext() 和 next() 一次一个元素地遍历集合。你很少需要手写这种循环,但有一个重要的例外:它是在遍历时删除元素的安全方式。
it.remove() 会删除 next() 最后返回的那个元素,迭代器仍然有效。这是在手动循环中修改集合唯一被认可的方式。
陷阱:ConcurrentModificationException
如果在 for-each 循环中直接对集合本身调用 add 或 remove,就会得到 ConcurrentModificationException:迭代器发现集合在它脚下发生了变化,拒绝继续。这是初学者最常犯的错误之一,和任何运行时异常一样,除非你避免触发它,否则它会中止整个方法。
List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : nums) {
if (n % 2 == 0) {
nums.remove(Integer.valueOf(n)); // throws ConcurrentModificationException
}
}
解决办法几乎总是 removeIf,它用一行代码表达意图,并替你处理遍历:
removeIf 适用于任何 Collection,所以同样的调用也能清理 HashSet。
forEach 方法
每个集合还有一个 forEach 方法,它接收一个 lambda,并对每个元素运行它。这是比循环更偏函数式、表达式风格的替代写法,适合写简短的单行代码:
注意,Map.forEach 直接接收一个双参数的 lambda (key, value),不需要 entrySet()。做简单的操作时用 forEach;当循环体变长,或者需要用 break 提前退出时(lambda 做不到),就回到普通的 for 循环。
下一篇:方法
你已经把数据装进了集合,并用 Java 提供的各种方式遍历了它们。下一步是把行为封装起来:编写你自己的方法,给一段逻辑起个名字,传入输入,然后反复使用。这就是下一篇的内容。
常见问题
Java 中如何遍历列表?
最简洁的方式是增强 for 循环(for-each):for (String s : list) { ... }。它适用于任何 Collection,比如 ArrayList、HashSet 等。只有在确实需要位置时才用带 get(i) 的索引循环;需要在循环中删除元素时,就用 Iterator。
Java 中如何遍历 HashMap?
Map 不是 Collection,所以要遍历它的某个视图。通常的选择是 for (Map.Entry<K, V> e : map.entrySet()),一次遍历就能同时拿到键(e.getKey())和值(e.getValue())。也可以遍历 map.keySet() 只取键,或遍历 map.values() 只取值。
为什么循环时会出现 ConcurrentModificationException?
因为在 for-each 循环遍历集合的过程中,你调用了集合的 add 或 remove。for-each 循环底层使用 Iterator,它检测到集合的结构发生了变化。解决办法是通过 Iterator 自己的 remove() 方法删除元素,或者不用循环,改用 removeIf(...)。