详解为什么在foreach中不能进行remove和add操作

今天早上小熙在关注的公众号中看到了这一篇不错的技术分享,特此实践顺便阐述下自己的理解。

在阿里开发手册上有这样一条规定:
阿里遍历规范
阿里规范上没有给出详细解释。所以小熙就详细的说说我的借鉴和理解。

一. foreach循环

  1. 介绍:
    Java5之后引入的功能。使得遍历数组和集合更加简洁,无需获得数组和集合长度,无需根据索引来访问数组元素和集合元素。foreach循环自动遍历数组和集合的每个元素。所以通常也被称为增强for循环。

  2. 语法:

    for(type(元素类型) variableName(元素变量) : array | collection(遍历的容器对象))
    
    {
        //variableName 自动迭代访问每个元素...	
    }
    
  3. 演示foreach和for循环的遍历结果(其实是一样的)

    (1)编写代码:

    public static void main(String[] args) {
    
            // 普通for循环遍历
            List<String> stringListFor = getStringList();
            for (int i = 0; i < stringListFor.size(); i++) {
                System.out.println(stringListFor.get(i));
            }
    
            System.out.println("-----------------华丽的分割线----------------------");
    
            //增强for循环遍历
            for (String s:getStringList()
                 ) {
                System.out.println(s);
            }
    
        }
    
    
    
        private static List<String> getStringList(){
            List<String> list = new ArrayList<>();
    
            Collections.addAll(list,"程熙","yxg","chengxi","chengxi");
            return list;
        }
    

    (2)结果展示:
    在这里插入图片描述
    由上图可知,foreach循环和for循环的结果是一致的。而且更加简便。

    (3) foreach循环原理:

    3.1 这里小熙使用的是命令行操作,使用javac命令编译BianLi.java文件,得到BianLi.class文件

    $javac BianLi.java 
    

    3.2 使用javap命令,反编译BianLi.class文件

    $ javap -v BianLi
    

    3.3 将-v改为-c 输出分解后的代码,例如,类中每一个方法内,包含java字节码的指令,

    $ javap -c BianLi
    

    之后查看命令行中显示的代码,(注意这些操作文件中不能含中文,否则会报不可映射字符,小熙将必要的都改为了英文,这里只是验证foreach的底层是使用了迭代器后面会恢复)

    如图:
    foreach源码分析
    由上图可以看出(小熙将华丽的分割线中文改为了fengexian全拼),foreach的底层就是迭代器实现的
    当然还有其他工具能更方便更清楚的展示,如使用jad工具,小熙只是想使用底层命令行实现并查看下。

二. 重审问题

规范中指出不让我们在foreach循环中对集合元素做add/remove操作,那么,我们尝试着做一下看看会发生什么问题。

  1. 修改如图:
    修改后的代码
  2. 结果如图:
    修改后的结果
    由上图可知,普通for循环删除后没有报错(其实普通的for循环也不推荐,因为会有漏删,下面会详细介绍),而foreach循环删除元素确抛错:java.util.ConcurrentModificationException(修改并发异常),同理,增加也会抛这个错,那这是为什么呢,其实是因为触发了一个Java集合的错误检测机制——fail-fast 。

三. fail-fast

fail-fast,即快速失败,它是Java集合的一种错误检测机制。 当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。

同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。

  1. 那这又和foreach循环有什么关系呢?
    上文分析到了,foreach的底层是迭代器实现的,由debug发现,在迭代器中使用Iterator.next 会调用 Iterator.checkForComodification方法 ,而异常就是checkForComodification方法中抛出的。

    我们直接看下checkForComodification方法的代码,看下抛出异常的原因:

    异常源码
    那下面就解释下吧。

  2. 解释
    首先,我们要搞清楚的是,到底modCount和expectedModCount这两个变量都是个什么东西。

    通过翻源码,我们可以发现:

  • modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。

  • expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。

  • Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器就是Itr类的实例。

    他们之间的关系如下:
    实现
    3. 结论

    由上图可知,迭代器开始遍历之前该迭代器的expectedModCount (期望集合修改次数)已经被modCount(实际修改次数)赋值了,然而在foreach遍历中删除数据或添加数据都是对modCount(实际修改次数)的修改,又因为是调用集合类自己的方法,所以不会对expectedModCount (期望集合修改次数)进行修改(这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除或添加了),所以就造成了不等的情况,所以就会抛出一个java.util.ConcurrentModificationException(修改并发异常),用来提示用户,可能发生了并发修改。

四. 讨论下用法

  1. 接着说为什么正常的for循环不建议呢?

    因为这会发生漏删的情况,如下图:

    代码:
    漏删
    结果:
    漏删结果
    由上图可知我要删除集合中的chengxi,但是删除后还有,这就是漏删了,那这又是为什呢?

    因为普通for循环是根据索引删除的,由于两个相同值在相邻的位置,当删除第一个值之后,集合发生改变要重新排序索引(因为集合发生改变他的的底层**Object[]**要做位移操作,这里是要向前位移一个索引),所以后面那个要被删除的值就被挪到了删除值的索引位置,从而避免了删除也就造成了漏删。

    当然也可以解决,在每次删除值的时候让索引自减就好了,最好是倒序遍历,还有equals方法也要倒着写防止空指针。

    	 i--;
    
  2. 可以直接使用迭代器(会修改expectedModCount (期望集合修改次数))

    代码:

    		// 迭代器循环遍历
            List<String> stringListFor = getStringList();
            System.out.println("删之前:"+stringListFor);
    
            Iterator<String> iterator = stringListFor.iterator();
    
            while (iterator.hasNext()){
                if(iterator.next().equals("chengxi")){
                    iterator.remove();
                }
            }
    

    结果:
    迭代器结果

  3. 可以使用java 8的新特性filter过滤不要的

    代码:

    		// java 8新特性filter循环遍历
            List<String> stringListFor = getStringList();
            System.out.println("删之前:"+stringListFor);
    
            List<String> stringList8 = stringListFor.stream().filter(s -> !s.equals("chengxi")).collect(Collectors.toList());
    
            System.out.println("删之后:"+stringList8);
    
    

    结果:
    迭代器结果

  4. 其实foreach也可以,但是限制条件很多

    首先你很明确只删除一个元素,而且删除之后就直接结束循环,避免下一次使用Iterator.next就不会抛出异常了。

    代码:

    		//增强for循环遍历
            List<String> stringListForeach = getStringList();
            System.out.println("遍历之前"+stringListForeach);
    
            for (String s:stringListForeach
                 ) {
                if(s.equals("程熙")){
                    stringListForeach.remove(s);
                }
                //删除后就结束遍历,避免抛异常
                break;
            }
            System.out.println("遍历之后"+stringListForeach);
    

    结果:
    foreach结果
    当然还有很多其他方法,小熙目前就介绍这么多了,有兴趣的朋友可以自行了解下。

    希望本文对大家有用,如有疑问可以点击小熙的头像获取小熙的联系方式和小熙联系哦。

  • 23
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值