问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

不要在foreach循环里进行元素的remove/add操作。 remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

  • 正例
List list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (删除元素的条件) {
        iterator.remove();
    }
}
  • 反例
List list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}

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

上面这一段摘自《Java开发手册-嵩山版》编程规约-集合处理-第14条,最后一行的说明并没有给出答案。因此自己琢磨这验证一下。

代码验证

  • 尝试移除元素”1″
List list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}
System.out.println(list); //输出[2]
  • 尝试移除元素”2″
List list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
    if ("2".equals(item)) {
        list.remove(item);
    }
}
System.out.println(list);

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

这里涉及到一个语法糖-增强for循环,从错误日志中可以看出增强for循环遍历list时涉及到ArrayList$Itr。

上述代码经过编译器处理后的class文件进行反编译,得到如下的代码片段

List list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
    String item = (String)iterator.next();
    if ("2".equals(item)) {
        list.remove(item);
    }
}

问题定位

通过日志定位找到抛异常的代码

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

从上述这段代码可以得出当modCount!=expectedModCount导致抛出的异常。

摘录ArrayList$Itr的 部分代码

private class Itr implements Iterator {
        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;
        }

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

查看ArrayList$Itr可以发现expectedModCount在创建Itr时通过modCount进行赋值,除此之外只有在remove()方法中重新通过modCount进行赋值。

重新查看测试代码后,摘录ArrayList中的相关代码

public Iterator iterator() {
    return new Itr();
}
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; // clear to let GC do its work
}

通过上述这段代码可以定位到问题了, 在调用list.remove()->remove()->fastRemove()中 修改了modCount,导致下一次调用next()方法时报异常

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

分析到这里细心的朋友可能又有一个新的问题了,元素”2″明明已经是list中的最后一个节点了,为什么还会再次调用到next()方法呢?

答案是list.remove()操作后list的size值会减1,而迭代器的hasNext()是通过cursor和size是否相等来判断是否还有下一个元素的。

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

至此,移除元素”2″报错的原因就分析完毕了。下面开始分析移除元素”1″时,为什么程序没有报异常?

List list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
    String item = (String)iterator.next();
    if ("1".equals(item)) {
        list.remove(item);
    }
}

可以很容易的发现两段代码的区别在于移除的元素在list中所处的位置不一样。经过对移除元素”2″的代码分析可以得知,问题的症结在于list.remove()方法修改modCount参数。所以我们还是从这个方法开始分析。

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

通过上述代码可以发现fastRemove()的原理是通过数组拷贝的方式将后一个元素的值拷贝到当前元素所在的位置。拷贝结束后尾节点置null,size-1。

在下一次调用hasNext()时结果为false导致迭代结束。

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

通过打印元素可以发现,移除元素”1″以后,元素”2″并没有被打印出来。

问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

总结

  • 此问题出现的原因就是调用了list的remove(Object)方法而不是采用迭代器itr.remove()方法进行元素移除。
  • 遍历集合时需要对集合进行增删操作,统一采用迭代器的方式进行。
  • 所有的元素操作都是通过迭代器进行的,因此要进行并发操作时对迭代器加锁是比较合适的一种手段。

Original: https://www.cnblogs.com/snowmaples/p/16487587.html
Author: code-habbit
Title: 问题深究01——为什么不要在foreach循环里进行元素的remove/add操作?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/587099/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 【原创】K8S环境下研发如何本地调试?kt-connect使用详解

    K8S环境下研发如何本地调试?kt-connect使用详解 背景 注:背景有点啰嗦,讲讲一路走来研发本地调试的变化,嫌烦的可以直接跳过,不影响阅读。 2019年 我在的公司当时是个…

    Java 2023年6月13日
    090
  • springboot mybatis plus多数据源轻松搞定 (上)

    在开发中经常会遇到一个程序需要调用多个数据库的情况,总得来说分为下面的几种情况: 下面针对第一种情况,提供一个解决方案。 因为两个数据库的功能和结构不一样,所以可以根据功能和结构把…

    Java 2023年6月7日
    084
  • java 百度人脸识别接口调用配置

    package org.fh.util; <span class="hljs-keyword">import org.json.JSONObject…

    Java 2023年6月7日
    052
  • 深入理解Java的switch…case…语句

    switch…case…中条件表达式的演进 最早时,只支持int、char、byte、short这样的整型的基本类型或对应的包装类型Integer、Char…

    Java 2023年5月29日
    078
  • 【设计模式】Java设计模式-命令模式

    Java设计模式 – 命令模式 😄生命不息,写作不止🔥 继续踏上学习之路,学之分享笔记👊 总有一天我也能像各位大佬一样🏆 一个有梦有戏的人 @怒放吧德德🌝分享学习心得,…

    Java 2023年6月16日
    075
  • java保留小数点,数字格式化

    注意: 1、整数除法会取整,不会保留小数点,需要保留小数,转为float在除 方法1、使用字符串格式化 <span class="hljs-function&quo…

    Java 2023年6月13日
    099
  • 设计模式之策略模式

    策略模式的作用 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。 为什么要使用策略模式? 我们想象一下最常见的场景:排序。排序无非两种选择,…

    Java 2023年6月6日
    081
  • Get请求使用请求体传递参数会报400异常的问题

    Get请求使用请求体传递参数会报400异常的问题 问题描述:前端使用Get请求并且使用请求体传递参数,后端使用@RequestBody注解封装参数,这时会出现400的异常信息。解决…

    Java 2023年6月13日
    084
  • 策略模式、策略模式与Spring的碰撞

    策略模式是GoF23种设计模式中比较简单的了,也是常用的设计模式之一,今天我们就来看看策略模式。 实际案例 我工作第三年的时候,重构旅游路线的机票查询模块,旅游路线分为四种情况: …

    Java 2023年6月5日
    078
  • 解决springboot打不出业务log

    今天不知道咋回事,单独的module可以打出log,而start的web工程始终打不出来,直觉就是jar包冲突,log的包太多了,logback自己跪了,后来经过尝试,得出如下的组…

    Java 2023年5月30日
    0101
  • SpringMVC执行流程

    SpringMVC三大核心组件 HandlerMapping处理器映射器:建立地址与方法的映射。 HandlerMapping负责根据用户请求url找到Handler即处理器,sp…

    Java 2023年5月30日
    079
  • MyBatis

    == 1、#{}和${}的区别:== (1) #{}是参数占位符,MyBatis会将SQL中的#{}替换为 ?, 实际参数值替换 在SQL运行前会使用 PreparedStatem…

    Java 2023年6月5日
    059
  • Nginx 源码分析– 模块module 解析执行 nginx.conf 配置文件流程分析 二

    1 、获取全部参与编译的模块module 进行统计编号。 2 、根据module 模块的个数分配 配置信息资源的指针空间。 3 、创建NGX_CORE_MODULE 核心模块的配置…

    Java 2023年6月15日
    071
  • SpringBoot 源码解析 (二)—– Spring Boot精髓:启动流程源码分析

    本文从源代码的角度来看看Spring Boot的启动过程到底是怎么样的,为何以往纷繁复杂的配置到如今可以这么简便。 入口类 @SpringBootApplication publi…

    Java 2023年5月29日
    066
  • Spring Cloud Stream 简介

    一、概述 Spring Cloud Stream 是一个建立在 Spring Boot 和 Spring Integration 之上的框架,有助于创建事件驱动或消息驱动的微服务。…

    Java 2023年5月30日
    070
  • 【开源】 bsf.mvc spingboot的扩展

    自动requestmapping(无需配置)实现。 2. freemarker java扩展实现,使freemarker更加便于使用。 3. request 参数大小写兼容实现,传…

    Java 2023年6月8日
    078
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球