为什么iterator.remove不会抛出ConcurrentModificationException

时间:2014-07-21 02:10:51

标签: java collections foreach iterator

iterator.removelist.remove的不同之处在于list.remove抛出异常时迭代器不会抛出异常?最后两者都在修改集合大小。

请在此处忽略多线程。我只是谈论for-each循环和迭代器循环。据我所知 - 每个循环仅在内部创建迭代器。

我很困惑。

5 个答案:

答案 0 :(得分:25)

我认为您的意思是,如果您正在迭代列表,为什么list.remove()会导致ConcurrentModificationException被抛出而iterator.remove()却没有?

考虑这个例子:

    List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));

    for (Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
        if (iter.next().equals("b")) {
            // iter.remove();    // #1
            // list.remove("b"); // #2
        }
    }

如果取消注释第1行,它将正常工作。如果您取消注释第2行(但留下#1注释),则会导致后续调用iter.next()抛出ConcurrentModificationException

原因是迭代器是一个单独的对象,它对基础列表的内部状态有一些引用。如果在迭代器运行时修改列表,则可能导致迭代器行为异常,例如:通过跳过元素,重复元素,索引数组的末尾等等。它试图检测这样的修改,如果有的话,它会抛出ConcurrentModificationException

通过迭代器删除元素有效并且不会导致异常,因为这会更新基础列表引用列表内部的迭代器状态,因此一切都可以保持一致。

但是,iterator.remove()并没有什么特别之处可以让它在所有情况下都有效。如果有多个迭代器迭代在同一个列表中,则由一个进行的修改将导致其他列表出现问题。考虑:

    Iterator<String> i1 = list.iterator();
    Iterator<String> i2 = list.iterator();
    i1.remove();
    i2.remove();

我们现在有两个指向同一列表的迭代器。如果我们使用其中一个修改列表,则会中断第二个列表的操作,因此调用i2.remove()将导致ConcurrentModificationException

答案 1 :(得分:11)

ConcurrentModificationException不会抛出

Iterator.remove(),因为这是在迭代时修改集合的允许的方式。这就是Iterator ConcurrentModificationException所说的内容:

  

从底层集合中移除此迭代器返回的最后一个元素(可选操作)。每次调用next()时,只能调用此方法一次。 如果在迭代正在进行中以除了调用此方法之外的任何方式修改基础集合,则未指定迭代器的行为。

如果您以任何其他方式更改正在迭代的集合,那么您可能会遇到异常,具体取决于迭代器的实现以及您正在迭代的集合(或其他)。 (某些集合类不会给你Reference:检查相应的javadoc以查看它们如何指定他们的迭代器的行为)

如果在同一个集合中有两个迭代器,并且通过其中一个迭代器删除,则您也有可能获得异常。


  

iterator.remove与list.remove的不同之处在于,当list.remove抛出时,迭代器不会抛出异常吗?

协议是当你通过迭代器删除时,迭代器的实现能够更新其数据结构以考虑删除。相反,如果通过集合对象删除(或插入或替换),则无法更新迭代器数据结构以使其与集合保持同步。

为什么呢?因为它需要以下内容:

  • 需要从集合数据结构到所有现存迭代器对象的链接。此类链接存在内存泄漏风险或需要使用while对象以避免泄漏。

  • 对集合的每次更新,&#34;保持步骤&#34;每个迭代器都需要更新。这会使收藏更新变得更加昂贵。

还存在非并发集合类型未实现为线程安全的问题,因此如果集合和迭代器由不同的线程使用/更新,您也可能会出现异常。

这些问题激发了集合API的设计选择。


  

我只是谈论for-each循环和迭代器循环。据我所知 - 每个循环仅在内部创建迭代器。

这是正确的。 for-each循环实际上只是使用迭代器的 for (int i = 0; i < list.size(); i++) { if (...) { list.remove(i); } } 循环的语法糖。

另一方面,如果你使用这样的循环:

ConcurrentModificationException

你不会得到IndexOutOfBoundsException,但是你需要为你删除的元素调整索引变量,而另一个线程的更新可能会导致你跳过元素或访问它们一旦 2


2 - 甚至得到{{1}}。如果集合不是并发/正确同步的,那么你可能会遇到更严重的问题。

答案 2 :(得分:3)

因为它是抛出异常的迭代器。如果你打电话给List.remove(),它就不知道删除,只知道它已经发生了变化。如果你打电话给Iterator.remove(),它就会知道当前元素已被移除,以及如何处理它。

答案 3 :(得分:0)

这是一个示例,如果集合迭代器没有检查底层集合的修改,那么事情可能会出错。这就是ArrayLists迭代器的实现方式:

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

    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size) throw new NoSuchElementException();
        // ...
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        // ...
        ArrayList.this.remove(lastRet);
        // ...
        cursor = lastRet;
        lastRet = -1;
    }

让我们看一个例子:

List list = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator it = list.iterator();
Integer item = it.next();

我们删除第一个元素

list.remove(0);

如果我们现在要调用it.remove(),迭代器会删除 number 2 ,因为那是lastRet指向的字段。

if (item == 1) {
   it.remove(); // list contains 3, 4
}

这是不正确的行为!迭代器的契约声明remove()删除next()返回的最后一个元素,但在并发修改的情况下它不能保留它的合同。因此,它选择安全并抛出异常。

其他馆藏的情况可能更为复杂。如果您修改HashMap,它可能会根据需要增长或缩小。那时,元素将落到不同的桶中,迭代器会在重新散列之前保持指向桶的指针。

请注意iterator.remove()本身不会抛出异常,因为它能够更新两者自身的内部状态和集合。但是,在同一个实例集合的两个迭代器上调用remove()会抛出,因为它会使其中一个迭代器处于不一致状态。

答案 4 :(得分:0)

public class FactorialTask extends RecursiveTask<BigInteger> {
    private static final int THRESHOLD = 1000;
    private RangeFactorialCounter iterativeCounter = new LoopCounter();

    private Integer firstVal;
    private Integer lastVal;

    public FactorialTask(Integer from, Integer to) {
        super();
        this.firstVal = from;
        this.lastVal = to;
    }

    @Override
    protected BigInteger compute() {
        return count(firstVal, lastVal);
    }

    private BigInteger count(int from, int to) {
        int middle = (from + to) >> 1;
        if (to - from > THRESHOLD) {
            List<FactorialTask> tasks = Arrays.asList(new FactorialTask(from, middle), new FactorialTask(middle + 1, to));
            tasks.forEach(RecursiveTask::fork);
            return tasks.stream()
                    .map(RecursiveTask::join)
                    .map(BigInteger.class::cast)
                    .reduce(new BigInteger("1"), BigInteger::multiply);
        } else {
            return (from != to) ? countSequential(from, to) : new BigInteger(String.valueOf(from));
        }
    }

    private BigInteger countSequential(int from, int to) {
        return iterativeCounter.count(from, to);
    }
}

你可以看到以上3个案例

案例1:通过添加元素进行修改,因此当使用next()函数时,会导致ConcurrentModificationException。

案例2:使用retain()进行修改,因此当使用next()函数时,会导致ConcurrentModificationException。

案例3:抛出java.lang.IllegalStateException而不是ConcurrentModificationException。

输出:

public class ArrayListExceptionTest {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        list1.add("c");
        Iterator<String> it1 = list1.iterator();
        ArrayList<String> list2 = new ArrayList<String>();
        list2.add("a");
        try {

            while (it1.hasNext()) {
                list1.add(it1.next());
            }
        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
        it1 = list1.iterator();
        while (it1.hasNext()) {
            System.out.println(it1.next());
        }
        it1 = list1.iterator();
        try {
            while (it1.hasNext()) {
                if (it1.next().equals("a"))
                    list1.retainAll(list2);
            }

        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
        it1 = list1.iterator();
        while (it1.hasNext()) {
            System.out.println(it1.next());
        }
        it1 = list1.iterator();
        Iterator<String> it2 = list1.iterator();
        it1.remove();
        it2.remove();
    }
}