1. Java

Foreach 循环中进行元素的remove/add操作?

今天看java开发手册(华山版)时发现一个巨坑!!!

话说阿里搞的这个东西真是excited

手册上是这样描述的:

11. 【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (删除元素的条件) {
        iterator.remove();
    }
}
反例:
for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?

上面代码删除List中的“1”时候是没有问题的,但是当我测试他这个说明时

说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?

正例的代码就是正例,没有任何问题;反例就不行了直接报错:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
	at java.util.ArrayList$Itr.next(Unknown Source)
	at top.sencom.redis.Demo.RedisDemo.main(RedisDemo.java:22)

那就来仔细研究一下到底怎么回事!

问题首先出现在ava.util.ArrayList$Itr.next

哎,对于反例用的可是foreach循环啊,咋报错中有迭代器字样的东西(Itr),莫非这个foreach循环就是用迭代器实现的???!!!

通过询问度娘得到如下结果:https://blog.csdn.net/a596620989/article/details/6930479

通过这篇文章可以得出:foreach语法最终被编译器转为了对Iterator.next()的调用

这样的话就是来回翻一翻,瞅一瞅

java.util.ArrayList

java.util.AbstractList

于是乎就找到了这个地方:

private class Itr implements Iterator<E> {
        /**
         * Index of element to be returned by subsequent call to next.
         */
        int cursor = 0;

        /**
         * Index of element returned by most recent call to next or
         * previous.  Reset to -1 if this element is deleted by a call
         * to remove.
         */
        int lastRet = -1;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

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

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

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

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

当我们用foreach时迭代器初始时 int expectedModCount = modCount;

这里的modCount该值表示对List的修改次数,它是在父类java.util.AbstractList中定义的,查看ArrayList的add()和remove()方法就可以发现,每次调用add()方法或者remove()方法就会对modCount进行加1操作。

此时你在foreach循环里面执行list.remove(item);

元素是移除了,但是ArrayList 类中的remove()方法会使modeCount++;而foreach循环的迭代器中的expectedModCount 并没有++,这就会使的在迭代器调用next()方法时调用checkForComodification()方法中发现modCount != expectedModCount从而 throw new ConcurrentModificationException();

好,既然知道了错误出在什么地方;那么就想办法避免这个问题吧。

那就是在使用迭代器时移除元素需要使用迭代器的移除方法而非List的移除方法!

就像正例中那样即可!

 

其实在JDK中也有类似表述:

The iterators returned by this class’s iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove or add methods, the iterator will throw aConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

 

https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html

 

在回头来看问什么移除“1”可以?

这还得在分析一下:

“1”这个元素在list的倒数第二个当它被list的remove()方法移除之后

list的size()会减少;而cursor表示当前位置。

迭代器完成一次循环执行 public boolean hasNext() {
return cursor != size();
}

时就会返回fase从而终止循环:也就是说当你往列表中添加两个元素都为“1”时你会发现foreach循环只能删除一个。也就是第一个“1”.