Java循环陷阱:在for-each中删除集合数据导致的异常原理
当我们在遍历 Java 集合(例如 ArrayList)时,如果在遍历过程中尝试修改集合(例如添加或删除元素),就会遇到 ConcurrentModificationException
异常。大家可能会对这个异常感到困惑,因此在这篇文章中,我将详细解释这个异常的产生原理,以及如何正确地在遍历集合的同时修改它。
ConcurrentModificationException 的产生原理
在 Java 集合类中,有一个 modCount
字段,这个字段记录了集合的“结构修改次数”。每当我们添加或删除集合元素时,modCount
就会增加。
这是集合中add的源码
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到当add方法调用时modCount++
当我们使用
for-each
循环或者 Iterator
遍历集合时,会创建一个迭代器对象,这个迭代器对象有一个 expectedModCount
字段,其值在创建时被赋值成 modCount
来保持一致。
在遍历的时候,迭代器会在每次开始时检查 expectedModCount
是否等于 modCount
。如果这两个值不等,迭代器就会抛出 ConcurrentModificationException
异常。
下面是一个简单的代码示例,演示了在 for-each
循环中尝试修改 ArrayList
时会抛出 ConcurrentModificationException
异常:
public static void main(String[] args) {
List<String> arrayList = new ArrayList<String>();
arrayList.add("1");
arrayList.add("2");//modCount 为 2
Iterator<String> iterator = arrayList.iterator();
while(iterator.hasNext()) {
String list = iterator.next();
if("2".equals(list)){
arrayList.remove(list);//因为调用方是arrayList所以modCount会加1,但是**expectedModCount 还是2
}
}
System.out.println("arrayList:"+arrayList);
}
在这个代码中,在arrayList中调用了2次add方法所以modCount等于2。 循环开始时创建了一个迭代器,这时 expectedModCount
被设置为 modCount
的当前值,也就是 2。
然后在遍历过程中,我们尝试删除 "2",但是调用方是arrayList这导致 modCount
增加到 3,但 expectedModCount
的值仍然是 2,因此在下一次迭代开始时,迭代器抛出了 ConcurrentModificationException
异常。
大家可能会好奇,不是只有俩个值吗!为甚么会在循环一次?
这与 Java 的 ArrayList
的迭代器实现有关。为了解释这个问题,我们需要深入了解 ArrayList
的 Iterator
是如何工作的。
ArrayList
的 Iterator
在每次调用 next()
方法时,会检查是否还有更多元素。这是通过比较一个 cursor
的值(该值表示迭代器当前所在的位置)和 ArrayList
的大小来实现的。如果 cursor
不等于 ArrayList
的大小,那么 hasNext()
方法就返回 true
。
第一次循环进来 cursor
=0,size
=2,进行下一次循环,
cursor
=1,size
=2 ,进行下一次循环,注意:在刚才的代码中,在遍历到第二个元素时删除了一个数据
所以第三次判断 cursor
=2,size
=1 仍然为true
继续向下执行
迭代器会检查 modCount
是否等于 expectedModCount
。此时 modCount
已经增加到3,而 expectedModCount
仍然是2,因此迭代器会抛出 ConcurrentModificationException
异常。
正确的修改方法
那么,我们应该如何在遍历集合的过程中正确地修改它呢?其实,Iterator
为我们提供了 remove
方法,我们可以通过这个方法来删除当前元素。这个方法在删除元素后,会正确地更新 expectedModCount
的值,使其等于 modCount
,从而防止 ConcurrentModificationException
异常的产生。
以下是一个使用 Iterator
的 remove
方法在遍历过程中删除元素的代码示例:
javaCopy code
List<String> arrayList = new ArrayList<String>();
arrayList.add("1");
arrayList.add("2");
Iterator<String> iterator = arrayList.iterator();
while(iterator.hasNext()) {
String list = iterator.next();
if("2".equals(list)){
iterator.remove();
}
}
这段代码中,我们使用 iterator.remove();
来删除元素,而不是 arrayList.remove(list);
。这样,迭代器就能正确地跟踪 modCount
的变化,从而避免 ConcurrentModificationException
异常。
总结起来,如果我们想在遍历集合的过程中修改集合,应该使用 Iterator
的 remove
方法。如果我们需要在遍历过程中添加元素,应该选择其他方法,例如先收集需要添加的元素,然后在遍历结束后添加到集合中。
实际开发中可以用removeIf方法来做删除。
转载自:https://juejin.cn/post/7237389821102538813