面试官:为什么foreach中不允许对元素进行add和remove

我是程序汪

共 412字,需浏览 1分钟

 ·

2021-10-20 11:33


粉的读者遇到了一个比较经典的面试题,也就是标题上说的,为什么 foreach 中不允许对元素进行 add 和 remove。阿粉就这个问题深入分析一下为什么不让使用 add 和 remove,并且实际运行一下,我们来看一下。

ArrayList

我们先来看看 ArrayList 中如果我们使用了 add 和 remove 会出现什么样子的结果,然后我们分析一下。

public static void main(String[] args) {
        List list = new ArrayList<>();
        //把元素放到list里面去
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i + "");
        }
        for (String s: list) {
            if ("5".equals(s)){
                list.remove(5);
            }
            System.out.println(s);
        }
    }

我们先看看结果是什么样子的。

Exception in thread "main" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
 at java.util.ArrayList$Itr.next(ArrayList.java:861)

这时候就有人说,你为啥不直接用 iterator 迭代器遍历呢?其实说这话的,一般都是没去看过源码的,为什么这么说,如果你要是反编译出来 foreach 这一段代码,那么你肯定发现内部是使用迭代器实现的,既然这样,那好,我们再用迭代器遍历一下试试。

public static void main(String[] args) {
        List list = new ArrayList<>();
        //把元素放到list里面去
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i);
        }
       Iterator iterator = list.iterator();
           while(iterator.hasNext()){
               Integer integer = iterator.next();
               if(integer==5){
                   list.remove();   //注意这个地方
               }
           }
    }

那结果如何呢?结果是一样的,还是会有异常的出现。

Exception in thread "main" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
 at java.util.ArrayList$Itr.next(ArrayList.java:861)

都出现了相同的异常 ConcurrentModificationException ,既然它已经给我们提示出异常的位置了,那么我们就来看看 ArrayList 的源码中,是什么样子的。

异常位置

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

这个地方告诉我们如果 modCount 不等于 expectedModCount 的时候,就会抛出这个异常信息,那么这两个参数都代表了什么东西呢?为什么不相等的时候,就会出现异常呢?

这时候就要让我们去看源码了在我们点到这个变量的时候,就会有注释告诉我们了 modCount 是 AbstractList 类中的一个成员变量,该值表示对List的修改次数

这时候我们来看看 remove 方法中是否对这个变量进行了增减。


public E remove(int index) {
        rangeCheck(index); //检查index是否合法

        modCount++; //modCout直接++
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

 public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // 设置为null方便GC
    }

大家可以看到,在 remove 的方法中,实际上只是对 modCount 进行了++,那 expectedModCount 又是个什么东西呢?

通过remove方法删除元素最终是调用的fastRemove()方法,在fastRemove()方法中,首先对modCount进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将size进行减1操作,并将引用置为null以方便垃圾收集器进行回收工作。

expectedModCount 是 ArrayList 中的一个内部类——Itr中的成员变量。

我们来找找源码。

int expectedModCount = modCount;

而 expectedModCount 表示对ArrayList修改次数的期望值,它的初始值为 modCount。

我们看一下 ArrayList 中的内部类是怎么给他赋值了,毕竟他的初始值是 modCount,而这个内部类就是 iterator 。

从源码可以看到这个类的next和remove方法里面都调用了一个checkForComodification方法,他是通过判断modCount和expectedModCount是否相等来决定是否抛出并发修改异常.

final int expectedModCount = modCount;

也就是说,expectedModCount 初始化为 modCount 了,但是后面 expectedModCount 他没有修改呀,而在 remove 和 add 的过程中 modCount 是进行了修改了的,这就导致了如果执行的时候,他就会通过 checkForComodification 方法来判断两个是否相等,如果相等了,那么没问题,如果不相等,那就给你抛出一个异常来。

而这也就是我们通俗说起来的 fail-fast 机制,也就是快速检测失败机制。

而这种 fail-fast 机制也是可以避免的,比如再拿出来我们上面的代码,

public static void main(String[] args) {
        List list = new ArrayList<>();
        //把元素放到list里面去
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i);
        }
        System.out.print("没有删除元素前"+list.toString());
        Iterator iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==5){
                iterator.remove();   //注意这个地方
            }
        }
        System.out.print("删除元素后"+list.toString());
    }

这样的话,你就发现是可以运行的,也是没有问题的,我们看运行结果:

没有删除元素前[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

删除元素后[0, 1, 2, 3, 4, 6, 7, 8, 9]

结果也是显而易见的,我们实现了在 foreach 中进行 add 和 remove 的操作.

其实还有一种方式 那就是 CopyOnWriteArrayList ,这个类也是能解决 fail-fast 的问题的,我们来试一下,

public static void main(String[] args) {
        CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
        //把元素放到list里面去
        for (int i = 0 ; i < 10 ; i++ ) {
            list.add(i);
        }
        System.out.print("没有删除元素前"+list.toString());
        Iterator iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==5){
                list.remove(5);   //注意这个地方
            }
        }
        System.out.print("删除元素后"+list.toString());
    }

我们运行后结果是一样的,

没有删除元素前[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

删除元素后[0, 1, 2, 3, 4, 6, 7, 8, 9]

他实现了对这个元素中间进行移除的操作,那么他的内部源码是怎么实现的,实际上很简单,复制

也就是他创建一个新的数组,再将旧的数组复制到新的数组上,但是为什么很少有人推荐这种做法,根本原因还是 复制

因为你使用了复制,那么就一定会出现有两个存储相同内容的空间,这样消耗了空间,最后进行 GC 的时候,那是不是也需要一些时间去清理他,所以阿粉个人不是很推荐,但是写出来的必要还是有的,毕竟是个人建议,各位看官看是不是会如何在 foreach 中去 remove了?


< END >

程序汪资料链接

程序汪接的7个私活都在这里,经验整理

Java项目分享  最新整理全集,找项目不累啦 04版

堪称神级的Spring Boot手册,从基础入门到实战进阶

卧槽!字节跳动《算法中文手册》火了,完整版 PDF 开放下载!

卧槽!阿里大佬总结的《图解Java》火了,完整版PDF开放下载!

字节跳动总结的设计模式 PDF 火了,完整版开放下载!

欢迎添加程序汪个人微信 itwang008  进粉丝群或围观朋友圈

浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报