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

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

public static void main(String[] args) {List<Integer> 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.ConcurrentModificationExceptionat 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 方法中是否对这个变量进行了增减。

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

2.2、expectedModCount 是什么?

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

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

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

2.3、熟悉的checkForComodification方法

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

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

2.4、流程回顾

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

也就是说,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<Integer> list = new ArrayList<>();for (int i = 0; i < 5; i++) {list.add(i);}System.out.println("没有删除元素前"+list.toString());// 迭代器使用listIterator和iterator均可ListIterator<Integer> 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类的

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<Integer> 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);// 存放元素enewElements[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内部类。

 在COWIterator内部类中是不支持remove、set、add操作的,至少我使用的jdk1.8是不支持,会直接抛出UnsupportedOperationException异常:

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

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

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

    来源 | 公众号 我是程序汪 阿粉的读者遇到了一个比较经典的面试题,也就是标题上说的,为什么 foreach 中不允许对元素进行 add 和 remove.阿粉就这个问题深入分析一下为什么不让使用 a ...

  2. foreach去除重复元素java_Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析...

    本文重点探讨 foreach 循环中List 移除元素造成 java.util.ConcurrentModificationException 异常的原因. 先看<阿里巴巴 Java开发手册&g ...

  3. C# - 在foreach中删除元素

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 在for ...

  4. 指针增量和数组的关系,指针偏移的补充,(重要面试),gdp调试,将数组中的n个元素逆序存放

    1.指针增量和数组的关系 //加1  代表了地址偏移了一个类型的字节数(整形数偏移四个字节,char形数偏移了一个字节) 再来下标法: 2.指针偏移的补充 也可以换一种写法(第12行) 但是呢同样的代 ...

  5. java list 遍历 删除元素_java中List遍历删除元素相关做法和注意事项

    首先,使用简单的for循环时,list.remove()肯定是没问题的,只要注意一下下标别越界就行. 示例代码: List ll = new ArrayList(); ll.add("1&q ...

  6. Java循环中删除一个列表元素

    本文主要想讲述一下我对之前看到一篇文章的说法.假设跟你的想法有出入,欢迎留言.一起讨论. #3. 在循环中删除一个列表元素 考虑以下的代码.迭代过程中删除元素: ArrayList<String ...

  7. Java forEach中 Lambda Expr中的 final变量要求

    https://my.oschina.net/wadelau/blog/1859419 Java forEach中 Lambda Expr中的 final变量要求 Java8闭包 闭包是一个函数在创建 ...

  8. java 查找链表中间元素_如何在Java中一次性查找Java中链表的中间元素

    如何在一次传递中找到LinkedList的中间元素?这是一个 Java 和非Java程序员面试时经常被问到的编程问题.这个问题类似于检查回文或计算阶乘,有时也会要求编写代码.为了回答这个问题,候选人必 ...

  9. php遍历中记录所有,php遍历类中包含的所有元素的方法

    这篇文章主要介绍了php遍历类中包含的所有元素的方法,涉及php中getConstants方法及数组操作的相关技巧,需要的朋友可以参考下 本文实例讲述了php遍历类中包含的所有元素的方法.分享给大家供 ...

最新文章

  1. 空缺十年,百度再任命CTO!加盟十年,王海峰现集AI大权为一身
  2. make 调试和传参
  3. DCMTK:父元素处理的测试程序
  4. 大二上学期总结计算机专业,计算机学生的大二第二学期自我总结-自我总结
  5. 作为微软技术.net 3.5的三大核心技术之一的WCF虽然没有WPF美丽的外观
  6. 算法不会,尚能饭否之顺序表
  7. Delphi源码排列
  8. sketchup 2018下载与安装教程
  9. GB/T19001—2008质量管理体系要求、标准、贯标(贯彻标准)
  10. Jmeter ramp-up 参数含义 以及http请求测试流程零基础
  11. *TEST 9 for NOIP 先打暴力(100/300)
  12. 真正的小说 真正的生活 真正的蜕变 真正的品味
  13. Introduction to Graph Neural Network(图神经网络概论)翻译:目录总览
  14. 【每日最爱一句】2013.07.10
  15. 华东师范大学副校长周傲英:未来,中国需要什么样的数据库?
  16. EXCEL中如何使用正则表达式
  17. 大连医科大学中山学院计算机科学与技术,2021年大连医科大学中山学院各专业录取分数线...
  18. LabVIEW控制Arduino驱动数码管(基础篇—11)
  19. LQBv16-Python:用莱布尼兹公式计算圆周率π
  20. MT4错误代码最完整汇总

热门文章

  1. Canal-adapter的简单配置
  2. 英文邮件:greetings
  3. 亏麻了,华为离职后年终奖照旧发。。
  4. 台阶的意思_台阶词语解释
  5. linux nfs 拒绝挂载,mount:被服务器拒绝的nfs访问
  6. CNN卷积神经网络十二生肖识别项目(一)数据下载篇
  7. 描述统计—AppStore app分析
  8. windows进注册表快捷键
  9. oracle如果查询出的值为null记为0
  10. 从键盘读入一个字符,如果该字符是大写字母则转小写,如果该字符是小写字母则转大写,如果不是字符则输出不是字母。