【强制】不要在 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&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;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(),所以不会产生异常。

posted @ 2022-08-04 17:04  萝卜不会抛异常  阅读(359)  评论(0编辑  收藏  举报