前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为什么foreach中不允许对元素进行add和remove

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

作者头像
Java极客技术
发布2022-12-02 21:00:45
4630
发布2022-12-02 21:00:45
举报
文章被收录于专栏:Java极客技术

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

ArrayList

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

代码语言:javascript
复制
public static void main(String[] args) {
        List<String> 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);
        }
    }

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

代码语言:javascript
复制
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 这一段代码,那么你肯定发现内部是使用迭代器实现的,既然这样,那好,我们再用迭代器遍历一下试试。

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

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

代码语言:javascript
复制
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 的源码中,是什么样子的。

异常位置

代码语言:javascript
复制
 final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

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

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

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

代码语言:javascript
复制

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中的成员变量。

我们来找找源码。

代码语言:javascript
复制
int expectedModCount = modCount;

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

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

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

代码语言:javascript
复制
final int expectedModCount = modCount;

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

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

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

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

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

代码语言:javascript
复制
没有删除元素前[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 的问题的,我们来试一下,

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

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

代码语言:javascript
复制
没有删除元素前[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

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

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java极客技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ArrayList
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档