从ArrayList源码探究迭代时修改集合内容会抛出异常的原因

剖析为什么不能在遍历集合时对集合对象本身修改,以及ConcurrentModificationException异常的产生原因

作者 jooop 日期 2017-03-16
从ArrayList源码探究迭代时修改集合内容会抛出异常的原因

从ArrayList源码探究迭代时修改集合内容会抛出异常的原因

ConcurrentModificationException异常的产生:

在阅读《阿里巴巴 JAVA 开发手册 》一文时,其中有这样一条描述:

  • 7. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
  • 反例:

    List<String> a = new ArrayList<String>();
    a.add("1");
    a.add("2");
    for (String temp:a){
    if("1".equals(temp)){
    a.remove(temp);
    }
    }
  • 说明:这个例子的执行结果会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?

  • 正例:
    Iterator<String> it = a.iterator();
    while (it.hasNext()){
    String temp = it.next();
    if("2".equals(temp)){
    it.remove();
    }
    }

对上面描述的foreach循环方法中,将"1"改成"2"之后,报错如下:

java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)

同样的问题:

另外根据集合对象的for增强即为iterator方式循环的简化的原理,将正例的iterator方式循环中it.remove()改为a.remove(temp);也会有同样的报错。
例如:

Iterator<String> it = a.iterator();
while (it.hasNext()){
String temp = it.next();
if("2".equals(temp)){
a.remove(temp); //注意此处
}
}
运行结果:
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
异常定位到该方法中的String temp = it.next();

从源码解释异常的原因

根据异常信息抛出的位置为String temp = it.next();,可定位异常是由迭代器中的方法抛出的。
查看ArrayList的源码,提取出重点信息如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
protected transient int modCount = 0;
public void trimToSize() {
modCount++;
//………此处省略
}
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//………此处省略
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//………此处省略
}
public void add(int index, E element) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//………此处省略
}
public E remove(int index) {
modCount++;
//………此处省略
}
public boolean remove(Object o) {
//调用fastRemove()进行修改。
}
private void fastRemove(int index) {
modCount++;
//………此处省略
}
public void clear() {
modCount++;
//………此处省略
}
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

由以上源码可以看出:

  • ListArray中有:
    属性:int modCount = 0;(记录对集合的修改操作次数)
    内部类:Itr。(用来返回Iterator对象)
    方法:add()remove(int index)clear()trimToSize()等。(其操作都会修改modCount的值。)
  • 返回的Iterator对象中有expectedModCount属性,初始化时使其等于集合对象中的modCount
  • Iterator对象中next()remove()方法在执行时会比较modCountexpectedModCount的值,若不相等则抛出ConcurrentModificationException异常。
  • Iterator对象中remove()方法在调用集合中的方法remove(int index)将集合中的对象进行删除以后,会使expectedModCount = modCount;

简单的说就是集合对象迭代对象分别有一个计数器记录修改操作。使用迭代器中remove()next()方法前都会检查两个计数器是否一致,若不一致则抛出异常。迭代器中的计数器只在迭代对象创建和使用迭代器中的remove()方法后,才会和集合中的计数器同步。若在迭代中调用集合中的方法则会使两个计数器不同步

fail-fast机制

另外经过查阅以后得知该异常检测机制其实是fail-fast

  • “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

对开始时问题的进一步解释

根据前面对源码的分析,得知了 ConcurrentModificationException 异常产生的条件,只要在迭代过程中通过集合本身的方法进行操作就会抛出该异常,然而在最初例子中:

List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp:a){
if("1".equals(temp)){
a.remove(temp);
}
}
//结果:正常执行。

此时虽然是在迭代过程中使用集合本身的方法进行了修改,但是并没有抛出异常。
对上面的例子进行修改,再次测试

List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
a.add("3");
while (it.hasNext()){
String temp = it.next();
if("1".equals(temp)){
a.remove(temp);
}
}
/**
上面解释过,此方法等同于
for (String temp:a){
if("1".equals(temp)){
a.remove(temp);
}
}
*/
//结果:
//java.util.ConcurrentModificationException
//at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
//at java.util.ArrayList$Itr.next(ArrayList.java:851)
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
a.add("3");
while (it.hasNext()){
String temp = it.next();
if("2".equals(temp)){
a.remove(temp);
}
}
//结果:
//正常执行
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
a.add("3");
while (it.hasNext()){
String temp = it.next();
if("3".equals(temp)){
a.remove(temp);
}
}
//结果:
//java.util.ConcurrentModificationException
//at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
//at java.util.ArrayList$Itr.next(ArrayList.java:851)

使用断点调试一步步执行后,发现问题在于在对倒数第二个对象使用集合内方法进行移除以后,并不会再次进入while循环。此时it.next()并没有执行到,因此不会抛出异常。

再次从上面贴出的源码分析:
在内部类Itr中关于hasNext()方法的定义如下:

public boolean hasNext() {
return cursor != size;
}

  • cursor是Itr类中的属性,初始化为0,每当执行next()方法后cursor=cursor+1。
  • size则为集合类中的对象的数量。
  • 在上面的例子中,移除倒数第二个对象之前,size=3,cursor =2
    移除倒数第二个对象后,size=2,cursor =2
  • 执行hasNext()返回false,结束循环。

总结

上面的测试用例中,"1""3"为条件时抛出异常是因为fail-fast机制的存在,"2"为条件时没抛出异常是因为循环已经结束,并没有执行到会检测该异常的it.next()方法。