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

面试 – 为什么foreach中不允许对元素进行add和remove我们先来看看使用 foreach 遍历 ArrayList 过程中使用 add 和 remove 会出现什么样子的结果 然后再分析一下 publicstatic String args List Integer list newArrayList lt gt for inti 0 i lt 20 i list add i for Integerj list Integer

1、foreach遍历ArrayList过程中使用 add 和 remove

我们先来看看使用foreach遍历ArrayList过程中使用 add 和 remove 会出现什么样子的结果,然后再分析一下。

public static void main(String[] args) { List 
  
    list = new ArrayList<>(); for (int i = 0; i < 20; i++) { list.add(i); } for (Integer j : list) { if (j.equals(3)) { list.remove(3); } System.out.println(j); } } 
  

运行结果:

0 1 2 3 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) at test.Test.main(Test.java:12)

结果是出现了ConcurrentModificationException 异常,追踪下抛出异常的位置(ArrayList.java:911)

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

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

2、追根溯源

2.1、modCount是什么?

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

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

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

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

2.2、expectedModCount 是什么?

expectedModCount 是 ArrayList 中的一个内部类——Itr中的成员变量,我们来看下怎么又扯出个内部类Itr。

通过反编译可以发现foreach编译后内部是使用迭代器实现的。

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

 迭代器是通过list.iterator()实例化的,list.iterator()就返回了一个内部类Itr的对象,从源码中可以看到Itr实现了Iterator接口,同时声明了expectedModCount这个成员变量, expectedModCount 表示对ArrayList修改次数的期望值,它的初始值为 modCount。

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

2.3、熟悉的checkForComodification方法

从源码可以看到这个类的next和remove方法里面都调用了一个checkForComodification方法,看到checkForComodification是不是很熟悉,这不就是异常的抛出位置吗。

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

checkForComodification方法是通过判断modCount和expectedModCount是否相等来决定是否抛出并发修改异常。

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

2.4、流程回顾

通过查看编译后的class文件,可以看出大致流程如下:当j为3时,调用了remove方法,remove方法中修改了modCount值,然后再输出j值,再进入下一次循环,此时hasNext为true,进入循环体第一行代码,调用next方法,next方法再调用checkForComodification方法,然后发现expectedModCount和modCount不一致,最终抛出ConcurrentModificationException 异常。

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

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

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

3、避免fail-fast 机制

3.1、使用listIterator或iterator

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

public static void main(String[] args) { List 
  
    list = new ArrayList<>(); for (int i = 0; i < 5; i++) { list.add(i); } System.out.println("没有删除元素前"+list.toString()); // 迭代器使用listIterator和iterator均可 ListIterator 
   
     listIterator = list.listIterator(); while(listIterator.hasNext()){ Integer integer = listIterator.next(); if(integer==3){ listIterator.remove(); listIterator.add(9); } } System.out.println("删除元素后"+list.toString()); } 
    
  

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

没有删除元素前[0, 1, 2, 3, 4] 删除元素后[0, 1, 2, 9, 4]

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

这里有个注意点,迭代器使用listIterator和iterator均可,看源码可以知道 listIterator其实使用的ListItr内部类,ListItr是继承了Itr类的,同时自己封了一些方法,例如add,hasPrevious,previous等等。所以代码中的remove方法是Itr类的,add方法是ListItr类的

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

 listIterator和iterator区别:

  1. 使用范围不同,Iterator可以应用于所有的集合,Set、List和Map和这些集合的子类型。而ListIterator只能用于List及其子类型。
  2. ListIterator有add方法,可以向List中添加对象,而Iterator不能。
  3. ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator不可以。
  4. ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
  5. 都可实现删除操作,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。

3.2、使用CopyOnWriteArrayList

CopyOnWriteArrayList 这个类也是能解决 fail-fast 的问题的,我们来试一下:

public static void main(String[] args) { CopyOnWriteArrayList 
  
    list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 5; i++) { list.add(i); } System.out.println("没有删除元素前"+list.toString()); for (Integer integer : list) { if(integer.equals(3)){ list.remove(3); list.add(9); } } System.out.println("删除元素后"+list.toString()); } 
  

 运行结果:

没有删除元素前[0, 1, 2, 3, 4] 删除元素后[0, 1, 2, 4, 9]

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

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

因为你使用了复制,那么就一定会出现有两个存储相同内容的空间,这样消耗了空间,最后进行 GC 的时候,那是不是也需要一些时间去清理他,所以个人不是很推荐,但是写出来的必要还是有的。

3.2.1、CopyOnWriteArrayList的add方法

public boolean add(E e) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 元素数组 Object[] elements = getArray(); // 数组长度 int len = elements.length; // 复制数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 存放元素e newElements[len] = e; // 设置数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); } } 

处理流程如下:

  • 获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。
  • 根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入下一步骤。
  • 将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。

3.2.2、CopyOnWriteArrayList的remove方法

public E remove(int index) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] elements = getArray(); // 数组长度 int len = elements.length; // 获取旧值 E oldValue = get(elements, index); // 需要移动的元素个数 int numMoved = len - index - 1; if (numMoved == 0) // 移动个数为0 // 复制后设置数组 setArray(Arrays.copyOf(elements, len - 1)); else { // 移动个数不为0 // 新生数组 Object[] newElements = new Object[len - 1]; // 复制index索引之前的元素 System.arraycopy(elements, 0, newElements, 0, index); // 复制index索引之后的元素 System.arraycopy(elements, index + 1, newElements, index, numMoved); // 设置索引 setArray(newElements); } // 返回旧值 return oldValue; } finally { // 释放锁 lock.unlock(); } } 

处理流程如下:

  1. 获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length – index – 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②
  2. 先复制index索引前的元素,再复制index索引后的元素,然后设置数组。
  3. 释放锁,返回旧值。

注意

CopyOnWriteArrayList解决 fail-fast 的问题不是通过迭代器来remove或add元素的,而是通过list本身的remove和add方法,所以add的元素位置也不一样,迭代器是当前位置后面一个,CopyOnWriteArrayList是直接放到最后。

有想法的同学可以看看CopyOnWriteArrayList的listIterator和iterator,其实是一样的,都是返回的COWIterator内部类。

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

面试 - 为什么foreach中不允许对元素进行add和remove 在COWIterator内部类中是不支持remove、set、add操作的,至少我使用的jdk1.8是不支持,会直接抛出UnsupportedOperationException异常:

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

 先写到这儿,后面有空再补充。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/210474.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 上午7:35
下一篇 2026年3月19日 上午7:36


相关推荐

  • 狂神说Linux_狂神说docker笔记

    狂神说Linux_狂神说docker笔记Linux在服务器端,很多大型项目都是部署在Linux服务器上利用VM + Centos7搭建本地Linux系统你可以使用 man [命令]来查看各个命令的使用文档,如 :man cp。概念云服务器就是一个远程电脑Linux中一切皆文件根目录/,所有的文件都挂载在这个节点下/bin:bin是Binary的缩写, 这个目录存放着最经常使用的命令。/boot: 这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件。/dev : dev是Device(设备

    2022年8月8日
    9
  • 计算机操作系统试题及答案(带解析),计算机操作系统练习题及答案

    计算机操作系统试题及答案(带解析),计算机操作系统练习题及答案计算机操作系统练习题及答案第一章操作系统引论一 单项选择题 1 操作系统是一种 B A 应用软件 B 系统软件 C 通用软件 D 工具软件 2 操作系统是一组 C A 文件管理程序 B 中断处理程序 C 资源管理程序 D 设备管理程序 3 操作系统是计算机系统的核心软件 按功能特征的不同 可把操作系统分为 B E C 个人机系统 网络操作系统和分布式操作系统等基本类型 其中

    2026年3月20日
    3
  • C++ XML 库 TinyXML2 的基本使用

    C++ XML 库 TinyXML2 的基本使用0.前言TinyXML-2是一个简单,小型,高效的C++XML解析器,可以轻松集成到其他程序中,直接引用源文件的话只需要包含两个文件(h和cpp,此外还有个测试文件里面带有demo)。TinyXML-2解析XML文档,并以此为基础构建可读取,修改和保存的文档对象模型(DOM)。文档说,在解释XML时仅使用UTF-8,假定所有XML为UTF-8(看了下使用MSVC编译器时生成的XML文件文本编码使用的本地编码)。该库还支持打印到文件或内存,使用XMLPr

    2022年5月6日
    144
  • java怎么使用random函数,java中的random函数

    java怎么使用random函数,java中的random函数RandomAccessFile(“C:/MyFile1/Test.java”,”wr”)30、当使用FileInputStream类中的read()方法时,如果没有读入一个字节数据时,返回值为()DA、0……3.Coding:ImplementthesolutioninJava.4.Testing:Makesurethatthenumbersap…

    2022年6月1日
    40
  • 设计模式(三)建造者模式Builder(创建型)

    设计模式(三)建造者模式Builder(创建型)设计模式 三 建造者模 Builder http blog csdn net hguisu article details 1 概述在软件开发的过程中 当遇到一个 复杂的对象 的创建工作 该对象由一定各个部分的子对象用一定的算法构成 由于需求的变化 复杂对象的各个部分经常面临剧烈的变化 但将它们组合在一起的算法相对稳定 例子 1 买肯德基

    2026年3月19日
    3
  • 两分钟!Cursor+Claude3.7将任何内容生成可视化网页(附完整教程)

    两分钟!Cursor+Claude3.7将任何内容生成可视化网页(附完整教程)

    2026年3月16日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号