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

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();
            }
        }
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值