ArrayList删除元素时导致的java.util.ConcurrentModificationException错误的分析及源码解读

news2024/11/15 12:11:47

1.前言

  集合对于开发者来说都不陌生,可以说是我们日常开发中使用最频繁的对象之一,尤其是ArrayList,可是对于一些开发者并不真正了解它,只是使用习惯了,也就按照集合中基础的一些api使用了,但有时候却因为错误的使用集合导致代码的性能较差,甚至出现致命错误的代码。
  前几天在做代码review的时候,发现有同事提交了这么一段代码,它的意图就是从文章列表中删除标题不合法的的文章。
  下面我简单给大家看一下(这里去掉了一些附属的代码,只做基本代码的说明):

 List<Article> articleList = new ArrayList<>();
        Article article = new Article("xxx");
        articleList.add(article);
        articleList.add(article);
        articleList.add(article);
        articleList.add(article);
        String removeTitle = "xxx";
        for (Article a : articleList) {
            if (removeTitle.equals(a.getTitle())) {
                articleList.remove(a);
            }
        }

  这位同事还不是很服气,觉得这么写没多大问题,之前很多代码就是这么写的啊。基于此,我们从头分析一下。

2.ArrayList

2.1 ArrayList 类的层次结构

在这里插入图片描述
  ArrayList实现了List、RandomAccess、Cloneable、Serializable接口,继承了AbstractList抽象类。通过实现RandomAccess接口,可以实现集合的随机访问;通过实现Cloneable、Serializable接口,可以实现克隆和序列化。

public class ArrayList<E> extends AbstractList<E>
     implements List<E>, RandomAccess, Cloneable, java.io.Serializable

2.2 ArrayList 属性及底层实现

  public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  	 private static final long serialVersionUID = 8683452581122892189L;

    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // non-private to simplify nested class access

    private int size;
}

  ArrayList主要有size(数组长度)、elementData(底层对象数组)、DEFAULT_CAPACITY(初始容量,默认10)、EMPTY_ELEMENTDATA(底层共享的空数组实例)。基于此,数组底层其实就是基于数组来实现的,并且使用数组来实现动态扩容。
  如果我们仔细看它的源码 ,会发现比较奇怪的地方,就是elementData属性加上了transient修饰(禁止序列化),可是ArrayList明明实现了Serializable接口啊。这是因为ArrayList的数组是基于动态扩容,并不是所有被分配的数组空间 都存在元素,所以如果采用外部的序列化方法,就会序列化整个数组,这就导致这些没有存储数据的内存空间也会被序列化;相反,ArrayList内部提供了两个私有方法writeObject以及readObject来自我完成序列化和反序列化,从而节省内存空间。

2.3 ArrayList的构造函数

  ArrayList一共有三个构造函数:
  1.List list= new ArrayList<>();默认构造函数,创建一个空数组对象:

  			 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    		}

  2.List list= new ArrayList<>(20);传入一个初始容量值的构造函数:

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

  3.传入一个集合类型进行初始化:

 HashSet<String> set = new HashSet<>();
        set.add("a");
        set.add("b");
        set.add("c");
        set.add("a");
        List<String> list= new ArrayList<>(set);

  源码如下:

      //传入一个集合类型进行初始化。
      public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

2.4 ArrayList的基本方法

2.4.1 ArrayList获取元素 list.get(i)

  由于ArrayList是底层是基于数组实现的, 实现了随机访问接口,所以在获取元素的时候是非常快的。

 public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
          E elementData(int index) {
        return (E) elementData[index];
    }

2.4.2 ArrayList新增元素

   ArrayList有两种新增元素的方法:
  1.add(E e):直接将元素加入到数组的末尾;

     public boolean add(E e) {
        	ensureCapacityInternal(size + 1);  // Increments modCount!!
        	elementData[size++] = e;
        	return true;
    }

  2.add(int index, E element);添加元素到任意位置(通过指定下标)

  	public void add(int index, E element) {
        rangeCheckForAdd(index);
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //进行数组元素的挪动,该位置后面的所有元素都需要重新排列
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
   			 }

  从源码可以看到,这两个方法在添加元素之前都会检查确认容量大小,如果容量不够大,就会按照原来数组的1.5倍进行动态扩容,扩容之后将数组复制到新的数组中。同时我们我们还可以看出,添加元素到任意位置,会导致该位置后面的所有元素都需要重新排列,而将元素添加到数组的末尾,在没有发生扩容的前提下,是不会有元素复制排序的过程。所以我们在初始化时如果知道了存储数据的个数,可以指定数组的容量大小,这样可以避免数据的动态扩容;同时,添加元素的时候从末尾添加,避免元素的重排。我们可以考虑  从以上这两个方法来提高性能。

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

        private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
        private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

2.4.3 ArrayList删除元素 remove(Object o)

 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
    }

  从源码中可以看到,ArrayList删除元素与添加元素到任意位置的方法有相同之处,ArrayList每次删除元素后,都要进行数组的重排(除非从尾部删除),删除的元素的下标越小,数组重排的开销就越大。

2.4.4 ArrayList 遍历

2.4.4.1 使用下标索引遍历 for(; ; )

	 for (int i = 0; i < list.size(); i++) {
	            System.out.println(list.get(i));
	        }

2.4.4.2 使用foreach遍历 for(😃

    for (String s : list) {
        System.out.println(s);
    }

2.4.4.3 使用迭代器遍历

    Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
          System.out.println(iterator.next());
    }

  但其实使用foreach遍历和使用迭代器遍历是一样的,使用foreach遍历,代码编译的时候也会转变成迭代器遍历:

Iterator iterator = list.iterator();
while(iterator.hasNext()) {
    String s = (String)iterator.next();
    System.out.println(s);
}

3. 错误分析及解决

  最初那段代码执行报错:

    Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.zyxds.Article.main(Article.java:38)

  那么为什么呢?从我们上面的对ArrayList的分析来看,这段代码最终会被编译器优化成如下:

	    List<Article> articleList = new ArrayList();
        Article article = new Article("xxx");
        articleList.add(article);
        articleList.add(article);
        articleList.add(article);
        articleList.add(article);
        String removeTitle = "xxx";
        Iterator var7 = articleList.iterator();

        while(var7.hasNext()) {
            Article a = (Article)var7.next();
            if (removeTitle.equals(a.getTitle())) {
                articleList.remove(a);
            }
        }
    }

  即foreach被优化成了迭代器.而迭代器中的next()方法,会检查modCount与expectedModCount是否相等:

            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];
        }
       final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

  但是我们看删除方法articleList.remove(a);它调用了articleList的删除方法,然后通过fastRemove()方法进行删除:

 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
    }
    

  在fastRemove()方法中,仅仅改变了modCount的值,而并没有体现expectedModCount的变化,因为expectedModCount是属于Itr,即Iterator迭代器的属性:

        private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
        }

  那应该怎么正确删除呢?首先使用迭代器遍历,然后调用迭代器的删除方法就可以了。

    List<Article> articleList = new ArrayList<>();
        Article article = new Article("xxx");
        articleList.add(article);
        articleList.add(article);
        articleList.add(article);
        articleList.add(article);
        String removeTitle = "xxx";
        Iterator<Article> itr = articleList.iterator();
        while (itr.hasNext()) {
            Article nextArticle = itr.next();
            if (removeTitle.equals(nextArticle.getTitle())) {
                itr.remove();
            }
        }

  我们这里顺便看下迭代器的删除方法的源码:

 public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                //会设置expectedModCount,使其等于modCount
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/360080.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MacBook IDEA 顶部菜单栏不显示问题

文章目录背景&#xff1a;当前显示方式一1. 快捷键&#xff1a;双击shift 搜索 idea.vmoptions3. 在idea.vmoptions文件末尾添加 -Dapple.laf.useScreenMenuBarfalse方式二1. 访达 > 应用程序 > idea 右键 显示包内容2. 进入到bin包位置的命令终端3. 编辑文件 vi idea.pr…

硬件学习 软件Cadence day05 快速拥有 元件的原理图和PCB 分装 (Ultra Librarian 下载)

1.下载 Ultra Librarian &#xff08;实测有用&#xff09; 一个链接&#xff1a; (5条消息) Ultra Librarian 的安装和使用_lian123的博客-CSDN博客_ultra librarian 安装过程简单&#xff01;&#xff01;&#xff0c; 一直next 就行. 2.快速寻找 元器件 &#xff08;…

8个优秀图片素材库,免费/商用/高分辨率。

有深度的摄影照片&#xff0c;总可以让我们深思&#xff0c;从中获取有用的设计灵感。图片素材可以用于很多场景&#xff0c;比如海报背景、封面等等。如何才能找到质量高、免费还可以商用的图片素材呢&#xff1f;别着急&#xff0c;本期我就给大家推荐8个非常牛的图片素材网站…

音视频基础之视频主要概念

视频主要概念 **视频码率&#xff1a;**kb/s&#xff0c;是指视频文件在单位时间内使用的数据流量&#xff0c;也叫码流率。码率越大&#xff0c;说明单位时间内取样率越大&#xff0c;数据流精度就越高。 **视频帧率&#xff1a;**fps&#xff0c;通常说一个视频的25帧&…

查找算法复习

先序在了解查找算法之前&#xff0c;需要熟悉几个概念&#xff0c;不然后面容易产生理解错误。查找表&#xff1a;即被查找的对象&#xff0c;通常由几个关键字组成。关键字&#xff1a;就是数据项、字段的意思。关键字有主次之分&#xff0c;其中主关键字取值是唯一的。查找长…

工程监测多通道振弦模拟信号采集仪VTN的AABB 通讯协议

工程监测多通道振弦模拟信号采集仪VTN的AABB 通讯协议 AABB 通讯协议是一种非标准自定义协议&#xff0c; 相较于 MODBUS 通讯协议&#xff0c;结构更简单&#xff0c;指令生成方法更容易&#xff0c;便于进行快速测试。 AABB 通讯协议支持单寄存器读写两种指令。 &#xff08…

开机一键ghost重装系统如何操作

现在有很多的朋友伙伴们在后台反映自己想尝试一个简单的重装系统方法&#xff0c;现在小编就带着推荐来啦&#xff0c;开机一键ghost重装系统&#xff0c;他简单操作易上手&#xff0c;小白也可以轻松掌握&#xff0c;大家一起来看看吧。 工具/原料&#xff1a; 系统版本&…

NLP文本自动生成介绍及Char-RNN中文文本自动生成训练demo

前言 文本自动生成是自然语言处理领域的一个重要研究方向&#xff0c;实现文本自动生成也是人工智能走向成熟的一个重要标志。文本自动生成技术极具应用前景。 例如&#xff0c;文本自动生成技术可以应用于智能问答与对话、机器翻译等系统&#xff0c;实现更加智能和自然的人机…

云生源安全引流方案(K8S)

背景 在云原生中,我们无法非常方便准确的截取流量。比如在 K8S 中的每个 Pod 都需要和 API Server 进行 health 通讯等,这些并非是我们用户真实发生的流量。所以我们需要把 K8S 中发生的流量和用户发生的流量给分割开来,还有就是 K8S 对于开发者来说是一个黑盒,不知道怎么…

如何写好controller层

前言一、Controller层参数接收二、统一状态码三、统一校验四、统一响应五、统一异常前言本篇主要要介绍的就是controller层的处理&#xff0c;一个完整的后端请求由4部分组成&#xff1a;1. 接口地址(也就是URL地址)、2. 请求方式(一般就是get、set&#xff0c;当然还有put、de…

运行redis报错 由于目标计算机积极拒绝,无法连接

第一打开redis服务器时出现闪退第二再点击redis-cli.exe时&#xff0c;出现了由于目标计算机积极拒绝&#xff0c;无法连接尝试用这个命令解决此问题&#xff1a;打开命令运行窗口&#xff0c;进入cmd&#xff0c;然后进入redis安装的目录&#xff0c;如下图&#xff1a;回车之…

“与众不同”的TOP250详细数据采集,pyecharts世界地图多维可视化展示

“与众不同”的TOP250详细数据采集&#xff0c;pyecharts世界地图多维可视化展示 前言&#xff1a; 本文描述爬取逗瓣250的电影详细信息&#xff0c;包括对电影名、评分、评论人数、电影名言、导演演员信息、电影年份、电影国家、电影类型等详细爬取&#xff1b; 并且针对爬…

EasyExcel 低内存导出大数据量的Excel方案探索 50万行 50列 (附:实现代码)

文章目录1.前言2.准备工作3.导出测试3.1.单次查询、全量导出3.2. 多次查询&#xff0c;多个文件&#xff0c;单次写入3.3.多次查询&#xff0c;多个文件&#xff0c;多次写入3.4.多线程导出探索3.5.文件打包成ZIP3.6.响应给客户4.实现代码5.结语1.前言 最近接到一个需求&#…

唤醒手腕 Java 后端 Springboot 结合 Redis 数据库学习笔记(更新中)

Redis 基本介绍 Redis Introduction The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker. 基本概念&#xff1a;redis 是一个开源的、使用 C 语言编写的、支持网络交互的、可基于内存也可持…

没有资源没有人脉,23年跨境电商仍值得入局!

随着经济全球化的不断深入&#xff0c;越来越多人关注到跨境电商行业。作为新兴的贸易业态&#xff0c;跨境电商拥有多边化、交易链条短等传统电商无法比拟的优势&#xff0c;能够有效地推广更多中国优质产品到全球市场上&#xff0c;在促进国家经济发展过程中发挥着举足轻重的…

谷歌推出新优化器Lion:优化算法的符号发现

文章目录谷歌推出新优化器Lion&#xff1a;优化算法的符号发现Lion VS AdamW论文实验1.图像分类2.视觉语言对比学习3.扩散模型4.语言建模和微调5.与其他流行优化器的比较超参数设置小结谷歌推出新优化器Lion&#xff1a;优化算法的符号发现 优化器即优化算法&#xff0c;优化器…

加入蓝精灵协会的快速指南

了解使用蓝精灵协会应用程序所需的所有要点。 什么是蓝精灵协会&#xff1f; 蓝精灵协会是唯一一个由蓝精灵官方品牌支持的 PFP 项目。 我们目前正处于这个交互式项目的第二阶段&#xff0c;重点是通过游戏化的 Web3 体验建立一个大型社区。下面是参与游戏的基本步骤&#xff0…

C++进阶:二叉搜索树

文章目录1 二叉搜索树概念2 二叉搜索树的实现2.1 结点的定义2.2 二叉搜索树的插入2.2 二叉搜索树的查找2.3 二叉搜索树的删除2.4 二叉搜索树的默认成员函数2.4.1 拷贝构造2.4.2 析构函数2.4.3 赋值重载3 二叉搜索树的应用3.1 k模型3.2 kv模型4 二叉搜索树的性能分析1 二叉搜索树…

【字典转模型 Objective-C语言】

一、点按钮,弹出的这个效果,这实际上是个Label, 这实际上是一个Label,点按钮弹出的这个效果, 设置一个Label的背景色、前景色、透明度、等等, 让它加进来,然后通过动画让它隐藏掉, 这就是,这个效果的实现思路, 咱们这个效果,先稍微往后放一放, 这个并不是重点…

匈牙利算法学习笔记

匈牙利算法学习笔记1. 前言1.1 二分图1.2 二分图匹配2. 匈牙利算法(Hungarian Algorithm)2.1 基础概念2.2 实现步骤参考链接&#xff1a;1. 14-4&#xff1a;匈牙利算法 Hungarian Algorithm1. 前言 1.1 二分图 二分图通常针对无向图问题。假设G(V,E)G(V,E)G(V,E)是一个无向图…