【强制】不要在 foreach 循环里进行元素的 remove/add 操作。
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
—— 《阿里巴巴编码规范》
反例:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
证明
首先先看"1"的运行结果
当"1"的时候,好像是没有什么问题的。然后我们看"2"的运行结果:
可以看到出问题了,一个java.util.ConcurrentModificationException
并发修改异常。
通过反编译,可以看到for循环在编译期间,被转译成了迭代器:
对于list.iterator(),它会返回一个ArraryList的内部类Itr的实例。
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator<E> iterator() {
return new Itr();
}
// ---------------------------------------------------------------------------
/**
* An optimized version of AbstractList.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;
Itr() {}
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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
可以看到抛出ConcurrentModificationException
的实际上是modCount != expectedModCount
这个条件。
modCount
:此列表在结构上被修改的次数。(The number of times this list has been structurally modified.Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results. )
expectedModCount
:此Itr实例内部记录的被迭代的列表修改次数。
正常迭代中:modCount = expectedModCount
条件的成立保证了被迭代的列表始终未被更改,迭代的数据始终是有效的。
换言之,当这个条件不成立时,即modCount != expectedModCount
,代表着被迭代的原列表结构已发生了改变。此时就会停止迭代,并抛出ConcurrentModificationException
。这实际上就是快速失败机制。
根据这个条件我们猜测,在迭代器进行迭代的过程中,进行了remove()
操作,破坏了modCount = expectedModCount
,触发了迭代器的快速失败机制,从而抛出异常。
找到源码,来验证我们的结论:
/**
* Removes the first occurrence of the specified element from this list,
* if it is present. If the list does not contain the element, it is
* unchanged. More formally, removes the element with the lowest index
* <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>
* (if such an element exists). Returns <tt>true</tt> if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return <tt>true</tt> if this list contained the specified element
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
可以看到在remove()
的过程中,存在modCount++
,使modCount != expectedModCount
,从而触发了快速失败机制!
可见,这就是不要在 foreach 循环里进行元素的 remove/add 操作的理由了。
那为什么"1"同样是进行了
remove()
操作却没有异常呢?
让我们回到最开始的示例。
我们已经知道增强for循环使用的迭代器进行遍历,使用hasNext()
作为迭代条件,让我们来Debug一下"1"的情况。
可以看到在运行完remove方法后,这时候modCount != expectedModCount
,但是这时候只执行了hasNext()
,判断了cursor != size
,所以这时候不会执行next()
,所以不会产生异常。