当索引只是碍事的时候
计数的 for 循环给你一个计数器、一个条件和一个更新步骤。但很多时候,你其实并不关心元素的位置,只是想从头到尾按顺序对每个元素做点什么。为此去管理索引纯属多余,而差一错误(off-by-one)正是从这里溜进来的。
for-each 循环(Java 里的正式名称是增强 for)完全去掉了计数器。你给出一个变量名,让它指向一个集合,循环就会依次把每个元素交给你。
基本语法
形式是 for (Type element : collection)。冒号可以读作“在……之中”:
没有 i,没有 scores.length,也没有 scores[i]。每一轮中,score 就是下一个元素。循环对每个元素执行一次,元素用完时自动停止,你不可能越过末尾,也不可能少走一个元素。
遍历列表
同样的循环适用于任何可迭代的对象,包括 List、Set 和其他集合类型。元素类型写在变量名前面:
注意,你不需要知道也不需要关心 langs 底层是数组、链表还是别的什么,for-each 在它们上面的用法都一样。这才是它真正的优势:一种可读性好的语法,适用于所有集合。
用 var 省掉类型名
如果元素类型很长或者一看就知道,var 可以让编译器推断类型,你就不用重复写了:
不用 var 的话,这个循环变量的类型就是又长又绕的 Map.Entry<String, Integer>。var 让代码保持易读,而且类型仍然在编译时被完整检查,它不是松散的动态类型。
修改集合的陷阱
这条规则几乎人人都会踩:在 for-each 循环遍历集合时,不能向集合添加元素,也不能从中删除元素。这样做会抛出 ConcurrentModificationException:
List<String> items = new ArrayList<>(List.of("a", "b", "c"));
for (String item : items) {
if (item.equals("b")) {
items.remove(item); // throws ConcurrentModificationException
}
}
循环发现列表在它脚下发生了变化,于是直接退出,而不是悄悄跳过或重复某些元素。要安全地删除,就改用显式的 Iterator,它有一个循环知道的 remove() 方法:
常见的简写是 items.removeIf(item -> item.equals("b")),它用一个 lambda 表达式在一行内完成同样的事。
只能读取,不能重新赋值
另一个不太明显的限制:给循环变量赋值只会改变本地副本,不会改变集合。对于习惯了循环变量是“活引用”的语言的人来说,这一点很出人意料:
如果需要把值写回数组,你就需要索引,也就是要用经典的计数 for 循环:for (int i = 0; i < nums.length; i++) nums[i] = nums[i] * 10;。对于对象元素,你可以修改变量指向的那个对象(比如调用 setter),但不能在集合中把它替换掉。
break 和 continue 照样可用
for-each 是一个真正的循环,所以 break 和 continue 的行为和其他地方完全一样:break 跳出循环,continue 跳到下一个元素:
这段代码先后打印两次 keep:它跳过 "skip",并在 "stop" 处停下,根本到不了 "never"。所以你并没有被限制成必须访问每个元素,只是放弃了索引,换来更简洁的代码。
下一篇:数组
你已经遍历过好几次数组了,但还没有细说它到底是什么:带有 .length 字段、大小固定、按索引访问的容器。下一篇从头开始,系统地讲解数组:如何声明、它们一开始的默认值,以及固定大小的数组和可增长的 ArrayList 有什么不同。
常见问题
Java 中的 for-each 循环是什么?
for-each 循环(也叫增强 for)不用计数器就能遍历数组或集合的每个元素:for (Type item : collection) { ... }。可以读作“对 collection 中的每个 item”。当你只需要每个元素、从不需要索引时,它比计数的 for 循环更简洁。
Java 中 for 循环和 for-each 循环有什么区别?
经典的 for 循环使用显式计数器(for (int i = 0; i < arr.length; i++)),所以索引和方向都由你控制。for-each 循环 for (Type x : arr) 没有索引,它按顺序访问每个元素。只读、从头到尾遍历时用 for-each;需要索引、想跳过元素或要修改集合结构时,用计数循环。
为什么我的 for-each 循环抛出 ConcurrentModificationException?
因为你在用 for-each 遍历集合的同时,调用了它的 add() 或 remove()。循环检测到结构发生了变化,于是抛出异常,防止出现不确定的行为。要安全地删除元素,请使用显式的 Iterator 及其 remove() 方法,或者先把要删除的元素收集起来,在循环结束后再删除。