为什么这段代码没有引发ConcurrentModificationException异常?

10

我在了解如何避免ConcurrentModificationException异常时,发现了一篇文章。该文章的第一个代码示例与以下代码相似,这会导致该异常:

List<String> myList = new ArrayList<String>();
myList.add("January");
myList.add("February");
myList.add("March");

Iterator<String> it = myList.iterator();
while(it.hasNext())
{
    String item = it.next();
    if("February".equals(item))
    {
        myList.remove(item);
    }
}

for (String item : myList)
{
    System.out.println(item);
}

然后它进一步解释了如何通过各种建议来解决这个问题。

当我试图复现它时,我没有得到异常!为什么我没有得到异常?

4个回答

8
根据Java API文档Iterator.hasNext不会抛出ConcurrentModificationException
在检查"January""February"之后,您从列表中删除一个元素。调用it.hasNext()不会抛出ConcurrentModificationException,但返回false。因此,您的代码干净地退出。然而,最后一个字符串永远不会被检查。如果将"April"添加到列表中,则会按预期收到异常。
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

public class Main {
        public static void main(String args[]) {

                List<String> myList = new ArrayList<String>();
                myList.add("January");
                myList.add("February");
                myList.add("March");
                myList.add("April");

                Iterator<String> it = myList.iterator();
                while(it.hasNext())
                {
                    String item = it.next();
                    System.out.println("Checking: " + item);
                    if("February".equals(item))
                    {
                        myList.remove(item);
                    }
                }

                for (String item : myList)
                {
                    System.out.println(item);
                }

        }
}

http://ideone.com/VKhHWN


你已经很好地解释了正在发生的事情,但没有说明原因。而原因是:这是 ArrayList 迭代器类中的一个错误。 - T.J. Crowder
不回答这个问题。但并不像TJ所说的那样是一个bug。 - zedoo
我刚刚在我的回答中添加了对API文档的引用。它按设计工作。在这种情况下,hasNext不会抛出异常,但也不会返回true。因此,它解释了行为并回答了问题。 - bikeshedder
2
根据Java API文档,Iterator.hasNext不会抛出ConcurrentModificationException。扇自己一个耳光,我删除了我的答案。这是非常错误的,但显然有记录。 :-) - T.J. Crowder
2
实际上,即使 Iterator.next() 没有声明抛出 CME 异常。整个 ArrayList 类的 JavaDoc 只是说:_如果在迭代器创建后任何时候以除迭代器自己的 remove 或 add 方法之外的任何方式对列表进行结构修改,则迭代器将抛出 ConcurrentModificationException 异常_,但没有具体指定哪个方法。 - Natix
显示剩余2条评论

5

来自源码中的ArrayList(JDK 1.7):

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();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

每次对ArrayList进行修改操作时,都会增加modCount字段的计数值(自创建以来列表已被修改的次数)。
在创建迭代器时,它将当前的modCount值存储到expectedModCount中。逻辑是:
  • 如果在迭代过程中根本没有修改列表,则modCount == expectedModCount
  • 如果列表被迭代器自己的remove()方法修改,则modCount会增加,但是expectedModCount也会增加,因此modCount == expectedModCount仍然成立
  • 如果其他方法(甚至是其他迭代器实例)修改了列表,则modCount会增加,因此modCount != expectedModCount,这导致了ConcurrentModificationException
但如您从代码中可见,检查并未在hasNext()方法中执行,只在next()方法中执行。hasNext()方法还仅仅比较当前索引和列表大小。当您从列表中删除倒数第二个元素("February")时,这导致以下调用hasNext()仅返回false并在抛出CME之前终止了迭代。
但是,如果您删除除倒数第二个元素以外的任何元素,则异常将被抛出。

2
我认为正确的解释来自于ConcurrentModificationExcetion的javadocs中的这个摘录:
注意,无法保证快速失败行为,因为在未同步并发修改的情况下不可能做出任何硬性保证。快速失败操作仅尽力而为地抛出ConcurrentModificationException。因此,编写依赖此异常进行正确性的程序是错误的:仅应使用ConcurrentModificationException检测错误。
因此,如果迭代器是快速失败的,则可能会抛出异常,但不能保证。尝试在您的示例中将 February 替换为 January ,则会抛出异常(至少在我的环境中)。

当阅读该段落时,请注意其中的警告。与此同时,bikeshedder在这里抓住了问题的关键所在:hasNext不会抛出ConcurrentModificationException!面对这个简单的事实,我在我的答案中的所有分析(以及你在我们的答案中的分析)都是无关紧要的。 - T.J. Crowder

0
迭代器会在检查并发修改之前,检查是否已遍历了剩余元素的数量,并且是否已经到达了结尾。这意味着,如果您只删除倒数第二个元素,那么在同一个迭代器中不会看到并发修改异常(CME)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接