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

文章深入分析了ArrayList的内部实现,包括其构造函数、属性、添加与删除元素的方法以及遍历方式。特别指出,通过迭代器遍历并删除元素时,直接调用ArrayList的remove方法会导致并发修改异常。正确的做法是使用迭代器自身的remove方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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();
            }
        }
<think>嗯,用户问的是关于Java中的ConcurrentModificationException异常,具体出现在ArrayList的Itr.next方法。我得先回忆一下这个异常通常发生的情况。记得这个异常通常在使用迭代器遍历集合的候,如果集合被修改了,比如增加或删除元素,就会抛出这个异常。比如用for-each循环遍历ArrayList,如果在循环体内部进行了结构性修改,就会触发这个错误。 那用户提供的堆栈信息显示是在ArrayList的Itr.next方法中抛出的,说明是在迭代过程中出了问题。需要解释为什么会发生这种情况。首先,得说明ArrayList的迭代器是fail-fast的,一旦检测到并发修改,就会立即抛出异常,而不是继续执行,避免不确定的行为。 接下来,应该举几个常见的导致这个问题的例子。比如,在遍历直接调用集合的add或remove方法,或者在多线程环境下没有同步的情况下修改集合。用户可能是在单线程中不小心在遍历修改了集合,或者是在多线程环境中出了问题。 然后,解决方案需要分情况讨论。单线程情况下,可以使用迭代器自己的remove方法代替集合的remove方法。或者改用CopyOnWriteArrayList这样的并发集合,或者遍历不要直接修改原集合,而是记录需要修改的元素,遍历结束后再处理。如果是多线程的情况,可能需要使用同步机制,比如synchronized块或者使用并发容器。 可能用户还不太清楚具体怎么操作,所以需要给出代码示例,比如正确的使用迭代器remove方法,或者使用CopyOnWriteArrayList的例子。同提醒常见的误区,比如for-each循环中调用remove的情况,或者多线程中的并发修改。 最后,总结一下这个异常的原因和解决办法,帮助用户更好地理解和避免这个问题。要确保语言通俗易懂,结构清晰,分步骤说明,让用户能一步步排查和解决自己的问题。</think>### 分步解析 `ConcurrentModificationException` #### 1. **异常本质** 这个异常表示**集合在迭代过程中被意外修改**,导致迭代器的预期行为被破坏。常见于单线程或多线程环境下对集合的“非同步修改”。 --- #### 2. **触发条件(核心原因)** 当以下两个条件同满足会触发异常: - **条件 1**:使用迭代器(如 `Iterator`、`for-each` 循环)遍历集合 - **条件 2**:遍历期间直接调用集合的 `add()`、`remove()` 或 `clear()` 等方法修改集合结构 代码反例: ```java List<String> list = new ArrayList<>(Arrays.asList("A", "B")); for (String s : list) { list.remove(s); // 直接调用集合的remove() → 触发异常 } ``` --- #### 3. **底层机制** - **modCount 机制**:ArrayList 内部维护 `modCount`(修改计数器) - **expectedModCount**:迭代器初始化会记录 `modCount` 的副本 - **检查逻辑**:每次调用 `next()` 或 `remove()` 校验 `modCount != expectedModCount` 源码片段(简化): ```java final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } ``` --- #### 4. **解决方案** **方案 1:使用迭代器的删除方法(单线程场景)** ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (条件) { it.remove(); // 通过迭代器删除 → 更新expectedModCount } } ``` **方案 2:使用并发集合类(多线程场景)** ```java List<String> safeList = new CopyOnWriteArrayList<>(list); for (String s : safeList) { if (条件) { safeList.remove(s); // 允许安全修改 } } ``` **方案 3:遍历记录修改(批量处理)** ```java List<String> toRemove = new ArrayList<>(); for (String s : list) { if (条件) toRemove.add(s); } list.removeAll(toRemove); // 遍历结束后统一处理 ``` **方案 4:使用同步控制(多线程场景)** ```java synchronized(list) { Iterator<String> it = list.iterator(); while (it.hasNext()) { // 操作代码 } } ``` --- #### 5. **常见误区** - **误区 1**:认为 `for-each` 循环可以安全修改集合 - **误区 2**:在多线程环境中未使用并发容器 - **误区 3**:混合使用 `Stream API` 与集合修改操作 --- #### 6. **扩展知识** - **Fail-Fast 机制**:快速失败策略保证程序及暴露问题 - **Fail-Safe 迭代器**:`ConcurrentHashMap`、`CopyOnWriteArrayList` 使用副本机制 - **Java 9+ 改进**:`List.of()` 创建的不可变集合无需考虑此问题 通过理解这些机制,可以有效避免和解决此类并发修改异常问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值