集合在迭代过程中能否添加、删除或修改元素

4,353 阅读3分钟
  1. 使用 for 循环
List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.size());
            if ("1".equals(list.get(i))){
                list.add("4");
                list.remove("1");
            }
        }
  1. 使用 foreach 遍历
List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (String s : list){
            if ("1".equals(s)){
                list.add("4");
                list.remove("1");
            }
        }
  1. 使用 Iterator 迭代器
List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            if ("1".equals(iterator.next())) {
                iterator.remove();
                list.add("4");
                list.remove("1");
            }
        }

在第一种情况下编译和运行都是可以的,第二种和第三种则会抛出 java.util.ConcurrentModificationException 的异常,这是为什么呢?
下面一段是来自百度知道的解释:
逻辑上讲,迭代时可以添加元素,但是一旦开放这个功能,很有可能造成很多意想不到的情况。
比如你在迭代一个 ArrayList,迭代器的工作方式是依次返回给你第0个元素,第1个元素,等等,假设当你迭代到第5个元素的时候,你突然在ArrayList的头部插入了一个元素,使得你所有的元素都往后移动,于是你当前访问的第5个元素就会被重复访问。
java 认为在迭代过程中,容器应当保持不变。因此,java 容器中通常保留了一个域称为 modCount,每次你对容器修改,这个值就会加1。当你调用 iterator 方法时,返回的迭代器会记住当前的 modCount,随后迭代过程中会检查这个值,一旦发现这个值发生变化,就说明你对容器做了修改,就会抛异常。
我们先看第三种情况,即使用 Iterator 迭代器对集合进行遍历,我们以 AbstractList 为例。
首先来看一下AbstractList是如何创建Iterator的,AbstractList有一个内部类:

private class Itr implements Iterator<E> {
        ...
    }

而创建Iterator需要调用iterator()方法:

public Iterator<E> iterator() {
        return new Itr();
    }

所以在调用集合的iterator方法之后实际上返回了一个内部类的实例。 我们看一下Itr这个类的next()方法是如何实现的:

public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

checkForComodification()方法的实现如下:

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

modCount表示集合的元素被修改的次数,每次增加或删除一个元素的时候,modCount都会加一,而expectedModCount用于记录在集合遍历之前的modCount,检查这两者是否相等就是为了检查集合在迭代遍历的过程中有没有被修改,如果被修改了,就会在运行时抛出ConcurrentModificationException这个RuntimeException,以提醒开发者集合已经被修改。 这就说明了为什么集合在使用Iterator进行遍历的时候不能使用集合本身的add或者remove方法来增减元素。但是使用Iterator的remove方法是可以的,至于原因可以看一下这个方法的实现:

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

可以看到,在集合的元素被remove之后,expectedModCount被重新赋值,是的modCount总是等于expectedModCount,所以不会抛出ConcurrentModificationException异常。

而上面提到的第二种使用foreach来对集合进行遍历本质上和第三种情况是一样的,因为根据Oracle提供的文档 ,foreach内部的实现机制其实就是使用的Iterator。

第一种使用for循环进行遍历时内部使用的就是集合本身的遍历方法,这里不做讨论。