当使用迭代器(Iterator
)或增强 for 循环(for-each loop)遍历一个集合时,如果通过集合自身的方法(如 list.add()
, list.remove()
)修改了集合的结构(增加或删除元素),而不是通过迭代器自身的方法,那么在下一次尝试访问元素时,就会立即抛出此异常。
错误代码示例:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for (String item : list) {
if ("b".equals(item)) {
list.remove(item); // 错误!在遍历时通过 list 对象直接修改
}
}
// 此代码将抛出 ConcurrentModificationException
以 ArrayList
为例,其“快速失败”机制依赖于两个核心变量:
modCount
(Modification Count)
add()
, remove()
等方法时,modCount
会自增。expectedModCount
(Expected Modification Count)
Iterator
内部的一个成员变量。当一个集合创建其迭代器时,会把当前集合的 modCount
值赋给这个 expectedModCount
。检测流程:在迭代过程中,每次调用迭代器的 next()
或 hasNext()
方法时,它都会比较当前集合的 modCount
是否仍然等于它自己保存的 expectedModCount
。
ConcurrentModificationException
,以避免在数据不一致的情况下继续操作,从而防止潜在的、无法预期的错误。以下是在遍历时安全修改集合的几种推荐方法:
remove()
方法这是在单线程环境下,遍历时修改集合的唯一官方推荐方式。
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("b".equals(item)) {
iterator.remove(); // 正确!使用迭代器的方法进行删除
}
}
创建一个原集合的副本来进行遍历,然后在原始集合上进行修改。这种方式简单直接,但会产生额外的内存开销。
// 在 new ArrayList<>(list) 这个副本上进行遍历
for (String item : new ArrayList<>(list)) {
if (item.contains("a")) {
list.remove(item); // 在原始 list 上进行修改
}
}
对于多线程环境或需要高并发读写的场景,应使用 java.util.concurrent
包下的并发集合,如 CopyOnWriteArrayList
。
List<String> safeList = new CopyOnWriteArrayList<>(list);
for (String item : safeList) {
if ("b".equals(item)) {
safeList.remove(item); // 安全,但开销较大
}
}
Stream API 提供了函数式的方式来处理集合,它不会在原地修改原集合,而是生成一个新的集合,从根本上避免了此问题。
// filter 会返回一个不包含 "b" 的新 stream,然后 collect 生成新 List
List<String> newList = list.stream()
.filter(item -> !"b".equals(item))
.collect(Collectors.toList());