大聪明教你学Java | 为什么不要在 foreach 里执行删除操作?

7,705 阅读4分钟

前言

今天在看阿里开发手册的时候发现里面提到了一个要求,强制要求开发人员不要在foreach循环里进行元素的remove/add操作。 在这里插入图片描述 看到这个要求的时候我突然想到之前使用foreach循环执行元素删除的时候会报错,不过一直没有研究过错误到底从何而来。

再次看到了这个问题,一下就激起了我的求知欲,今天就来个打破砂锅问到底,好好研究一下到底为什么不能在foreach循环里进行元素的删除操作。

开启打破砂锅问到底模式

public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("打破");
        list.add("打破");
        list.add("砂锅问到底");
        for (String str : list) {
            if ("打破".equals(str)) {
                list.remove(str);
            }
        }
        System.out.println(list);
 }

这段代码看起来一切正常,但是运行起来就报错了.... 在这里插入图片描述

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.edu.achievement.controller.Test.main(Test.java:19)

咱们根据错误信息,可以定位到 ArrayList.java 的第 909行代码。

在这里插入图片描述 也就是说咱们在执行元素删除时,调用到了checkForComodification方法,该方法对modCount和expectedModCount进行了比较,发现两者不等,就抛出了ConcurrentModificationException异常。

那么问题就来了,为什么会执行checkForComodification方法呢?modCount和expectedModCount又是什么呢? 麻爪ing....😔

皇天不负有心人,柳暗花明又一村,经过一顿百度操作后,发现了蛛丝马迹,原来for-each 本质上是个语法糖,底层是通过Iterator+while循环实现的。

这里简单说一下什么是语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。 P.S. 语法糖概念来自百度 说白了语法糖就类似于汉语中的成语,用更简练的言语表达一个比较复杂的含义。

言归正传,既然for-each的底层是通过Iterator+while循环来实现的,那么咱们回头再看看ArrayList.java中的iterator方法。 在这里插入图片描述 我们可以发现iterator方法的返回值是Itr,Itr又是什么呢?咱们接着往下看

/**
     * An optimized version of AbstractList.Itr
     */
    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
        int expectedModCount = modCount;

        Itr() {}

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

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

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

Itr是ArrayList.java中的一个内部类,它实现了Iterator接口,也就是说new Itr() 的时候expectedModCount被赋值为modCount,而modCount是List 的一个成员变量,表示集合被修改的次数。

P.S. modCount 定义在了AbstractList.java的第601行 在这里插入图片描述 这时候问题就迎刃而解了😊

原来由于 list 在一开始执行了 3 次add方法,add方法会先调用 ensureCapacityInternal方法,然后ensureCapacityInternal方法又调用了 ensureExplicitCapacity方法,在ensureExplicitCapacity方法中会执行modCount++的操作,也就是说我们之前执行了3次add方法后,modCount的值就变成了3,所以执行new Itr()的时候expectedModCount的值也就变成了3。

咱们接着往后看,在代码执行第一次循环时,发现“打破”等于str,于是就执行了list.remove(str),而remove方法又会调用fastRemove方法,此时问题的根源就找到了,咱们先看看fastRemove的源码

private void fastRemove(int index) {
    modCount++; // 修改次数加一
    int numMoved = size - index - 1; // 需移动元素的个数
    if (numMoved > 0) // 移动
    System.arraycopy(elementData, index+1, elementData, index,numMoved);
    // size--
    elementData[--size] = null; // clear to let GC do its work
}

这时候有些小伙伴应该已经发现问题的根源了,就是modCount++ !!!

此时modCount执行了加1的操作,那么modCount的值就变成了4,执行第二次循环时,会执行 Itr的next方法,next方法就会调用 checkForComodification方法,但是此时expectedModCount的值为3,所以抛出了ConcurrentModificationException异常,产生fail-fast事件。

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。(内容来自百度百科)

是不是又学习到了一个新知识,哈哈哈哈哈😊

所以如果我们需要删除元素的时候一定不要用foreach循环哦,我们换成Iterator就可以完美的执行删除操作了。

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("打破");
        list.add("打破");
        list.add("砂锅问到底");
        Iterator<String> itr = list.iterator();
        while (itr.hasNext()) {
            String str = itr.next();
            if ("打破".equals(str)) {
                itr.remove();
            }
        }
        System.out.println("list = " + list);
    }

在这里插入图片描述 这时候可能又有小伙伴会产生疑问,为什么使用Iterator的remove方法就可以避开fail-fast保护机制呢?

因为Iterator的remove方法中多了一句expectedModCount = modCount,就是这一句关键的代码,就完美的避开了fail-fast保护机制。

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

你在被打击时,记起你的珍贵,抵抗恶意; 你在迷茫时,坚信你的珍贵,抛开蜚语; 爱你所爱 行你所行 听从你心 无问东西