代码实现

方法一:for循环
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("ab");
list.add("abc");
list.add("abc");
list.add("abcr");
list.add("abc");
list.add("abcf");
list.add("abc");
list.add("abdc"); for(int i=0;i<list.size();i++) {
if(list.get(i).equals("abc")) {
System.out.println(i+":"+list.get(i));
list.remove(i); // 删除后 下标调整 导致漏删
}
}
System.out.println(list); }
结果:漏删!
2:abc
4:abc
5:abc
[a, ab, abc, abcr, abcf, abdc]
方法二: Iterator遍历,并使用自身的remove()删除元素
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("ab");
list.add("abc");
list.add("abc");
list.add("abcr");
list.add("abc");
list.add("abcf");
list.add("abc");
list.add("abdc"); Iterator<String> iter = list.iterator();
while(iter.hasNext()){
if(iter.next().equals("abc")){
iter.remove();
} } }
结果:
[a, ab, abcr, abcf, abdc]

原理

上述的两种方法,第一种方法导致漏删。第二种方法是正确的。那么为什么在用for循环遍历的时候删除元素,会导致漏删的情况呢?这是因为在for循环时,数组会调整数组的下标,会导致漏删。由上述代码可以看出,由于下标位置在不断调整,而i也在++ ,所有导致i=3位置的元素被跳过了。从而漏删。所以我们不能用for循环遍历的同时删除元素。正确的做法是利用for循环从后往前遍历,或者使用Iterator遍历,并调用自身的remove方法删除。Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性!

需要注意的是,使用Iterator遍历的时候,不能不允许并发调用ArrayList的remove/add操作进行修改,否则会抛出异常。这时为什么呢?

原理是:Iterator是工作在一个独立的线程中,而且拥有一个mutex锁。Iterator在建立后会创建一个指向原来对象的单索引链表。当原来的对象元素发生改变(增加或者删除一个元素),这个索引表是不会同步改变的。所以当索引指针往后移动的时候就找不到要迭代的对象,这时候就会触发fast-fail快速失败机制。

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。那么,如果你在遍历的时候,调用list的add()或者remove(),使得集合的结构发生改变。Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。

让我们看看源码,为什么会抛出这个并发修改异常?

从源码知道,每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:

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

此时,需要去看看ArrayList的源码。

可以看出,该方法才是判断是否抛出ConcurrentModificationException异常的关键。在该段代码中,当modCount != expectedModCount

时,就会抛出该异常。但是在一开始的时候,expectedModCount初始值默认等于modCount,为什么会出现modCount != expectedModCount,很明显expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(如modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用add/remove()使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。从而触发fast-fail机制。

避免fast-fail

方法一

在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。

方法二

使用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap。比如CopyONWriterArrayList, CopyOnWriterArrayList在是使用上跟 ArrayList几乎一样, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

对于HashMap,可以使用ConcurrentHashMap, ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。

参考链接:https://blog.csdn.net/zymx14/article/details/78394464

面试题:写一个遍历ArrayList的时候,删除一个元素的例子?并说说原理。的更多相关文章

  1. 写一段代码在遍历 ArrayList 时移除一个元素?

    该问题的关键在于面试者使用的是 ArrayList 的 remove() 还是 Iterator 的 remove()方法.这有一段示例代码,是使用正确的方式来实现在遍历的过程中移 除元素,而不会出现 ...

  2. Java中ArrayList问题:删除一个ArrayList中的重复元素,注意留意一个问题

    该问题有两种方法: 一 利用两个数组,此法简单,不讨论 二 利用一个数组,从第0个开始依次取元素,并在其后元素中查找是否有该元素,有则删掉后面的重复元素,依次遍历.---但是这种情况要特别注意,当后续 ...

  3. Java集合类ArrayList循环中删除特定元素

    在项目开发中,我们可能往往需要动态的删除ArrayList中的一些元素. 一种错误的方式: <pre name="code" class="java"&g ...

  4. 运行APP显示两个APP图标,一个打不开,删除一个后,另一个也会消失。

    可能原因:你添加了两个intent-filter 的LAUNCHER 事件,这种情况尤其在一个项目多个module的时候容易出现 <intent-filter>               ...

  5. java集合遍历删除指定元素异常分析总结

    在使用集合的过程中,我们经常会有遍历集合元素,删除指定的元素的需求,而对于这种需求我们往往使用会犯些小错误,导致程序抛异常或者与预期结果不对,本人很早之前就遇到过这个坑,当时没注意总结,结果前段时间又 ...

  6. k8s删除一个Node并重新加入集群

    k8s删除一个节点使用以下命令 删除一个节点前,先驱赶掉上面的pod kubectl drain 172.17.3.51 --delete-local-data 然后我们来删除节点 kubectl d ...

  7. 面试题-->写一个函数,返回一个数组中所有元素被第一个元素除的结果

    package com.rui.test; import java.util.Random; /** * @author poseidon * @version 1.0 * @date:2015年10 ...

  8. 参考JDK1.8源码,自己写一个类似于ArrayList的动态数组

    1. ArrayList的基本实现原理 ArrayLiST其内部用一个普通数组来存储数据,当此数组不够容纳新添加的元素的时候,则创建一个更大长度的新数组,并将原来数组中的元素复制到新数组中. 2.Ar ...

  9. 现在有一个长度20的SET,其中每个对象的内容是随机生成的字符串,请写出遍历删除LIST里面字符串含"2"的对象的代码。

    现在有一个长度20的SET,其中每个对象的内容是随机生成的字符串,请写出遍历删除LIST里面字符串含"2"的对象的代码. public class RemoveTwo { //le ...

  10. 遍历ArrayList时同时修改引发的问题

    看见一篇博客,没有写完整,于是增补了一下: 博客原文:http://www.cnblogs.com/alipayhutu/archive/2012/08/11/2634073.html 注:黄色字体为 ...

随机推荐

  1. 轻松实现H5页面下拉刷新:滑动触发、高度提示与数据刷新全攻略

    前段时间在做小程序到H5的迁移,其中小程序中下拉刷新的功能引起了产品的注意.他说到,哎,我们迁移后的H5页面怎么没有下拉刷新,于是乎,我就急忙将这部分的内容给填上. 本来是计划使用成熟的组件库来实现, ...

  2. 08-Python迭代器与生成器

    迭代器 什么是迭代器 迭代是Python最强大的功能之一,是访问序列中元素的一种方式. 迭代器是一个可以记住遍历的位置的对象. 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器 ...

  3. 05-Python函数

    函数定义与调用 函数由以下几个部分组成: 函数名 函数参数 函数体 返回值 定义一个函数: def showMyName(name): #定义函数 print(name) showMyName(&qu ...

  4. Grab 基于 Apache Hudi 实现近乎实时的数据分析

    介绍 在数据处理领域,数据分析师在数据湖上运行其即席查询.数据湖充当分析和生产环境之间的接口,可防止下游查询影响上游数据引入管道.为了确保数据湖中的数据处理效率,选择合适的存储格式至关重要. Vani ...

  5. STM32 学习:IAP有关介绍

    --- title: mcu-stm32-IAP-0-about date: 2020-05-27 08:51:58 categories: tags: - iap - stm32 - about - ...

  6. 设备树DTS 学习:3-驱动开发中常用的 DTS api

    背景 本章的内容是为了实现在驱动中的开发,通过调用有关的api来寻找设备树节点熟悉,从而达到使用设备树进行驱动开发的目的. 参考:Linux内核 设备树操作常用API Linux设备树语法详解一文中介 ...

  7. 中台框架模块开发实践-用 Admin.Core 代码生成器生成通用代码生成器的模块代码

    前言 之前分享中台 Admin.Core 的模块代码生成器,陆续也结合群友们的反馈,完善了一些功能和模板上的优化,而本篇将基于此代码生成器生成一个通用代码生成器模块的基本代码 后续再在此代码的基础上进 ...

  8. OpenCV程序练习(一):图像基本操作

    展示一张图片 代码 import cv2 img=cv2.imread("demoimg.png") #读取图像 cv2.imshow("demoName",i ...

  9. 小产品,快变现,Solo社区共建者 James 专访

    采访人:徐小夕. 本次受邀采访的嘉宾是Solo社区计划负责人&Solo社区联合创建者 James Pan(老潘). 专访内容 1. Solo社区创建的背景 随着国内软件市场内卷加剧,加上大环境 ...

  10. 【SQL】晨光咖啡馆,过滤聚合的微妙碰撞

    这天,小悦懒洋洋地步入办公楼下的咖啡馆,意外地与一位男子不期而遇.他显然因前一晚的辛勤工作而略显疲惫,却仍选择早到此地,寻找一丝宁静与放松.他叫逸尘,身姿挺拔,衣着简约而不失格调,晨光下更显英俊不凡, ...