列表会抛出ConcurrentModificationException异常,但是集合不会抛出ConcurrentModificationException异常吗?

12

我有以下两个Java类

import java.util.*;

public class ArrayListTest032 {
    public static void main(String[] ar) {
        List<String> list = new ArrayList<String>();
        list.add("core java");
        list.add("php");
        list.add("j2ee");
        list.add("struts");
        list.add("hibernate");

        Iterator<String> itr = list.iterator();

        while (itr.hasNext()) {
            System.out.println(itr.next());
        }
        list.remove("php");

        while (itr.hasNext()) {
            System.out.println(itr.next());
        }

    }
}

当我运行上述代码时,我得到以下输出。
core java
php
j2ee
struts
hibernate

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at ArrayListTest032.main(ArrayListTest032.java:20)

由于我在迭代时修改了列表,所以这是可以预料的。但在下面的Java类中,同样的逻辑是由set族执行的。
import java.util.*;

public class HashSetTest021 {
    public static void main(String[] ar) {
        Set<String> set = new HashSet<String>();
        set.add("core java");
        set.add("php");
        set.add("j2ee");
        set.add("struts");
        set.add("hibernate");

        Iterator<String> itr = set.iterator();

        while (itr.hasNext()) {
            System.out.println(itr.next());
        }
        set.remove("php");

        while (itr.hasNext()) {
            System.out.println(itr.next());
        }

    }
}

输出结果如下。

hibernate
core java
j2ee
php
struts

没有任何ConcurrentModificationException

我只是想知道为什么相同的代码在list系列中抛出ConcurrentModificationException,但在set系列中没有任何ConcurrentModificationException

6个回答

8
这是一种“倒退”的行为,因为一旦迭代器被完全遍历,它们就不能再次使用,也就是说,当您到达列表末尾时,它们的hasNext方法应该返回false。
在这种情况下,由ArrayList.iterator返回的迭代器是一个内部实现类,其hasNext代码如下:
public boolean hasNext() {
    return cursor != size;
}

因此,在第二个循环中调用hasNext时,它会(错误地)指示还有更多的项可以迭代,因为在第一次迭代后执行了改变列表大小的操作。从语义上讲,在达到列表末尾后,您不应该能够继续迭代列表中的项,但由于这个实现细节,它允许您继续进行第二个while循环。当然,在那时,由于您对支持列表所做的更改,会出现并发修改异常。
另一方面,您的哈希集使用的迭代器其hasNext实现如下:
public final boolean hasNext() {
    return next != null;
}

这个实现在迭代完成后被修改的哈希集合方面不是很“容易受攻击”,因此hasNext方法表现更好。


好的,如果是集合家族呢? - Rais Alam
@Real - 添加哈希集实现的详细信息。 - Perception

4
这是实现上的不同之处:数组列表返回的迭代器即使位于末尾,也会检测并发修改,因为它会检查长度;而哈希集合、树集和链表的迭代器则不会检测这种情况,因为它们会先检查是否位于末尾,再检查并发修改。文档允许迭代器在并发修改时不抛出异常,因此两种方法都是有效的。

注意:ArrayList迭代器实际上检查的是修改计数,而不是长度本身,因此如果您添加然后立即删除一个元素,使长度保持不变,之后仍然会出现ConcurrentModificationException异常。 - Alice Purcell

2
JavaDoc中开始阅读Iterator。它是否在任何地方提到ConcurrentModificationException?
现在,阅读ConcurrentModificationException的JavaDoc,并注意以下内容(已加粗):
该异常可能被抛出,由于检测到对象的并发修改是不允许的而导致。
现在仔细查看您的代码。您的while循环遍历集合的所有元素(即使您的第一个示例的输出没有显示这一点,这告诉我您已编辑了输出或者这不是您的实际代码)。在删除元素时,没有更多的项可以迭代,因此第二个循环应立即退出。
所以,结论是列表迭代器的实现者已经选择即使没有更多要迭代的元素也抛出该异常,而集合迭代器的实现者则选择不抛出该异常。两种情况都符合规格要求。

我已经在上面的代码中添加了相同的类,你可以运行代码并自行检查输出。 - Rais Alam
@Real - 是的,我运行了它,输出显示为“hibernate”,而你的帖子没有。但是,与其为一个括号注释而感到不安,你应该真正思考我的答案的内容 - parsifal
有什么想法,为什么这种列表抛出异常而集合不抛出异常的设计被优先考虑? - djechlin
@djechlin - 这不是设计,而是实现。我最初的评论是可能只是实现者的偏好,但我认为Perception有一个很好的解释(尽管我记得几年前最后一次查看它们时实现中有更多的代码)。 - parsifal
但是我的回答的关键点是观察到的行为都符合规范,因此没有充分的理由去质疑它。而且被质疑的原因很可能是OP依赖于未指定的行为。 - parsifal

1

如果您对HashSet进行任何操作,除非通过迭代器,否则可能会抛出ConcurrentModificationException异常。然而,有很多启发式算法围绕着迭代器的快速失败行为,目的是尽可能地完成迭代。JavaDocs在其行为方面似乎非常清晰。


1
 public static void main(String[] ar) {
            List<String> list = new ArrayList<String>();
            list.add("core java");
            list.add("php");
            list.add("j2ee");
            list.add("struts");
            list.add("hibernate");

            Iterator<String> itr = list.iterator();

            while (itr.hasNext()) {
                System.out.println(itr.next());
            }
            list.remove("php");

          /*  while (itr.hasNext()) {
                System.out.println(itr.next());
            }*/

        }

problem in itr object.it holds the list object reference

为什么Set不会抛出异常呢?你可以检查两个类。 - Rais Alam
从输出的堆栈跟踪中可以清楚地看到,当我们调用迭代器next()函数时,异常就会出现。如果你想知道迭代器如何检查修改,它的实现在AbstractList类中,其中定义了一个int变量modCount,它提供了列表大小已更改的次数。 - Biswajit

0

如果是列表,当我们使用第一个循环遍历它时 Iterator itr = set.iterator();

    while (itr.hasNext()) {
        System.out.println(itr.next());
    }

游标值和大小将变得相同。Cursor 包含遍历的总元素数,hashNext() 方法内包含以下代码用于列表遍历:

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

所以在第一个 while 循环之后,cursor == size。但是从列表中删除元素后,size 变为 (originalSize-1)。因此,在下一个 while 循环中,它进入 while 并且在 itr.next() 方法内部检查 modcount 修改并抛出 ConcurrentModificationException。

对于 Set,它在每个 itr.hasnext() 调用中检查 next != null。在遍历第一个 while 循环之后,next 变为 null。从集合中删除元素不会影响 next 值为 null,而 itr.hasNext 将返回 next == null 为 true,因此它不会进入 while 循环来检查 modcount 修改。因此,它不会抛出 ConcurrentModification Exception。


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