ArrayList是java开发时非常常用的类,常碰到需要对ArrayList循环删除元素的情况。这时候大家都不会使用foreach循环的方式来遍历List,因为它会抛java.util.ConcurrentModificationException异常。比如下面的代码就会抛这个异常:

List list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (String item: list) {
if (item.equals("3")) {
list.remove(item);
}
}
System.out.println(Arrays.toString(list.toArray()));

那是不是在foreach循环时删除元素一定会抛这个异常呢?答案是否定的。

见这个代码:
List list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (String item: list) {
if (string.equals("4")) {
list.remove(item);
}
}
System.out.println(Arrays.toString(list.toArray()));

这段代码和上面的代码只是把要删除的元素的索引换成了4,这个代码就不会抛异常。为什么呢?

接下来先就这个代码做几个实验,把要删除的元素的索引号依次从1到5都试一遍,发现,除了删除4之外,删除其他元素都会抛异常。接着把list的元素个数增加到7试试,这时候可以发现规律是,只有删除倒数第二个元素的时候不会抛出异常,删除其他元素都会抛出异常。

好吧,规律知道了,可以从代码的角度来揭开谜底了。

首先java的foreach循环其实就是根据list对象创建一个Iterator迭代对象,用这个迭代对象来遍历list,相当于list对象中元素的遍历托管给了Iterator,你如果要对list进行增删操作,都必须经过Iterator,否则Iterator遍历时会乱,所以直接对list进行删除时,Iterator会抛出ConcurrentModificationException异常

其实,每次foreach迭代的时候都有两步操作():

iterator.hasNext()  //判断是否有下个元素
item = iterator.next()  //下个元素是什么,并赋值给上面例子中的item变量
next()方法的代码如下:
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

这时候你会发现这个异常是在next方法的checkForComodification中抛出的,抛出原因是modCount != expectedModCount

modCount是指这个list对象从new出来到现在被修改次数,当调用List的add或者remove方法的时候,这个modCount都会增加;

expectedModCount是Iterator类中特有的变量,指现在期望这个list被修改的次数是多少次,这个值在调用list.iterator()创建iterator的时候初始化为modCount,该值在iterator初始化直到使用结束期间不会改变。

iterator创建的时候modCount被赋值给了expectedModCount,但是调用list的add和remove方法的时候不会同时修改expectedModCount,这样就导致下次取值时检查到两个count不相等,从而抛出异常。

解决这个问题的一种方式是使用Iterator来操作列表:

Iterator it = list.iterator();
while(it.hasNext()) {
if (it.next().equals("3")) {
it.remove();
}
}
那么为什么这种方式不会抛出该异常呢?下面是ArrayList中内部类Itr的remove方法:
public void remove() {
if (lastRet 
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

注意下面这句:

expectedModCount = modCount;

可以看出,在使用iterator()方法得到的Iterator对象后,通过iterator.remove方法是可以正确删除列表元素的,因为它保证了expectedModCount=modCount。

避免这个问题的另一种方法,是不使用foreach语句的for循环:

for (int i = 0; i 
String s = list.get(i);
if (s.equals("3")) {
list.remove(i);
continue;
}
i++;
}

回到问题上来,在使用foreach迭代ArrayList时,是可以删除任何一个元素的,且只能删除一个,而且这只能发生在迭代到倒数第二个元素的时候。比如下面的代码不会有异常:

List list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (String item: list) {
if (string.equals("4")) {
list.remove("5"); //删除的不一定是当前元素
}
}
System.out.println(Arrays.toString(list.toArray()));

其真正的原因是remove("5")这一句之后,下一次foreach语句将调用iterator.hasNext()方法,如果此时返回false,这样就不会进到next()方法里了,也就不会调用checkForComodification而导致异常了。

疑问:当循环到倒数第二个元素时,如果再多删除一个会怎样呢?比如:

for (String item: list) {
if (item.equals("4")) {
list.remove("1");
list.remove("5");
}
System.out.println(Arrays.toString(list.toArray()));
}

这段代码中,list是可以被打印出来的,因为list.remove()方法可以正确执行,其结果也是正确的。但是执行完这次打印,进入下一次迭代时,又产生了checkForComodification异常,还没想明白为什么。