它不会抛出ConcurrentModificationException异常。

10
我有以下代码,我期望它会抛出一个ConcurrentModificationException异常,但它却成功运行了。为什么会这样?
public void fun(){
    List <Integer>lis = new ArrayList<Integer>();
    lis.add(1);
    lis.add(2);

    for(Integer st:lis){
        lis.remove(1);
        System.out.println(lis.size());
    }
}

public static void main(String[] args) {
    test t = new test();
    t.fun();
}

为什么会抛出那个错误?ArrayList.remove() 不会抛出那个错误,只有索引越界。 - SyntaxTerror
1
嘘……你从没看到我对这个问题的回答 :) - Oliver Watkins
请在此处查看Java核心团队的解释:https://bugs.java.com/bugdatabase/view_bug?bug_id=4902078 - sankar
7个回答

10

List上的remove(int)方法会移除指定位置的元素。在开始循环之前,您的列表如下所示:

[1, 2]

然后在列表上启动一个迭代器:

[1, 2]
 ^

你的for循环然后删除了位置为1的元素,这个元素是数字2:

[1]
 ^

迭代器在下一次隐式调用hasNext()时返回false,循环终止。

如果您向列表添加更多元素,则会收到ConcurrentModificationException。然后,隐式的next()将引发异常。

请注意,从JCF的ArrayList的Javadoc中:

请注意,由于存在未同步的并发修改,无法保证迭代器的快速失败行为,通常情况下无法提供任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写依赖此异常来保证正确性的程序是错误的:迭代器的快速失败行为应仅用于检测错误

这实际上可能是Oracle ArrayList迭代器实现中的一个bug;hasNext()没有检查修改:

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

3

它不会抛出ConcurrentModificationException异常,因为正如vandale所说,迭代器只在next()方法中检查并发修改。下面是由ArrayList返回的Iterator实例的一部分:

    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];
    }

hasNext()仅查看游标是否指向列表的最后一个索引。它不检查列表是否已被修改。因此,您不会得到ConcurrentModificationException异常,它只会停止迭代。


1
如果你有一个包含3个元素的列表,例如:
lis.add(1);
lis.add(2);
lis.add(3);

在您的情况下,您将会遇到ConcurrentModificationException异常。 注:我已经尝试过了!

1
重点在于,正如 ArrayListConcurrentModificationException 中所述:

请注意,由于存在非同步并发修改,迭代器的快速失败行为无法保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。

现在是由 ArrayList 返回的 Iterator 的代码示例:
    public boolean hasNext() {
        return cursor != size;
    }

    public E next() {
        checkForComodification();
        <stuff>
        return <things>;
    }

    <more methods>

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

正如您可以清楚地看到的那样,在ArrayList的情况下,“尽力而为”的检查是在调用next()时进行的,而不是在调用getNext()时进行的。您的循环在没有第二次调用next()的情况下终止,因此没有异常。如果一开始有3个元素或添加一个元素,它将失败。还值得注意的是,如果使用反射修改数组列表而不更新modCount变量(淘气...),则根本不会抛出异常。 modCount也不是易失性的,这再次表明它仅是“尽力而为”,并且没有保证,因为迭代器可能无法看到最新的值。

1
因为您没有删除1,而是删除了索引为1的元素。(remove(int)remove(Object)之间的区别)
迭代器只会在调用next()时检查修改情况,而不是在hasNext()上进行检查,由于您已经删除了2,列表只有一个元素,因此退出。

2
实际上,如果您在索引0处进行删除,它也无法抛出异常。请在发布答案之前先测试一下。 - Ordous
1
@Ordous 这是同样的原理,循环将在检查列表是否已被修改之前退出。 - vandale
1
相同的原则,但第一句话完全无关紧要,而“因为”会让任何阅读它的人偏离实际原因。 - Ordous

0
在这个循环中:
   for(Integer st:lis){
        lis.remove(1);
        System.out.println(lis.size());
    }

你只是不断地从矩阵中删除索引为1的元素,甚至不关心st中的内容。因此,这个循环在每次迭代时都会尝试删除索引为1的项。并发修改将在这个循环中出现:

 for(Integer st:lis){
    lis.remove(st);
    System.out.println(lis.size());
}

2
这实际上是不正确的,因为他正在从索引1中删除该项。只有当Java将int 1自动装箱为Integer 1时,它才会删除值1。 - Jason Nichols
1
你说得对,我太蠢了。真不敢相信这个答案居然被点赞了两次。我会尝试修正它的。 - TheMP

0

你的列表中只有2个条目。因此,由于你正在删除一个条目,所以循环只运行一次。

如果修改了列表并且再次尝试对其进行某些操作,则会抛出ConcurrentModificationException异常。但在对其进行任何操作之前,我们已经退出了循环,因此没有异常。尝试向列表中添加另一个条目并运行程序,这将引发异常。


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