Java JDK源码分析之for循环删除异常原理以及解决办法

302 阅读4分钟

一、本地环境

  • 编辑器:IntelliJ IDEA 2017

  • JDK版本:jdk 1.8

二、引发的现象

在一些业务中,我们会用到遍历集合然后找到需要删除的元素进行remove,并且可能会删除多个元素。例如在管理群组时会踢人,踢人的业务逻辑大概是遍历这个群的人员关系,找到需要踢的人然后把他移除。

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

for (String s : list) {
    if ("b".equals(s)) {
        list.remove(s);
    }
}

比如这么一段代码,就会抛一下异常

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.lonelycountry.test3.Test1.test3(Test1.java:68)

但是有的同学比较幸运,写了下面这段代码,巧妙或者说是碰巧躲避了这个陷阱

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

for (String s : list) {
    if ("b".equals(s)) {
        list.remove(s);
        break;
    }
}

三、解决方案

1、在只需要删一次集合内部元素的代码上加上break

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

for (String s : list) {
    if ("b".equals(s)) {
        list.remove(s);
        break;
    }
}

2、使用迭代器

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

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = iterator.next();
    if ("b".equals(s)) {
        iterator.remove();
    }
}

四、查看源码找问题

1、遍历方法源码(这里会以ArrayList作为例子)

使用for循环遍历集合和使用迭代器遍历集合使用的是同一个方法,在ArrayList类中有个内部类实现了Iterator接口,有个叫next()的方法就是遍历方法(只截了部分方法)。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private class Itr implements Iterator<E> {
        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];
        }
    }
}

next()方法第一行调用了checkForComodification()方法,这个方法是用来校验有没有非法操作的。

final void checkForComodification() {
    //左边是操作次数,只要集合有了元素改变,就会modCount++,右边是预判操作数,
    //大家可能会想到了,就是调用某个方法时预判操作数和实际操作数没有同步,结果就出问题了
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

2、ArrayList自身的remove(Object o)方法源码分析

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    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
    }
}

remove(Object o)方法中,遍历找到了需要删除的索引,校验索引有效后,执行了fastRemove(int index)方法。这个方法第一步就是对实际操作数+1,然后进行了数组操作,而这时expectedModCount没有变化,可想而知,当再执行next()方法的时候,checkForComodification()方法肯定会抛出异常。

public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos, 
    int length);

在群组操作上使用了System类的arraycopy()方法,底层调用的应该是C或者C++的方法(我猜的),我查了下文档,大概说下逻辑。

index(索引)srcPos开始取src数组的元素,长度为length,然后覆盖dest数组的destPos位置(注意是覆盖,不是插入)。但是为什么在操作群组上用这么复杂的方法我就不得而知了,有对算法精通的骚年可以指导下我哦。

3、ArrayList迭代器内部类的remove()方法源码分析

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private class Itr implements Iterator<E> {
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }
}

这个方法其实本质还是调用了ArrayList自身的remove(int index)方法,和上面的方法大同小异,但是在后面偷偷做了个实际操作数和预判操作数同步,就是因为这行逻辑导致使用迭代器遍历中删除不会抛异常。肯定有人疑惑既然知道了原因,为什么不在remove(Object o)方法中加一行同步呢,这是不行的,因为expectedModCount变量是内部类Itr中的变量,而remove(Object o)方法是ArrayList外部声明的,无法操作干扰内部类的变量

五、总结

主要分析了两种遍历集合的代码区别,以及出错的位置和原因。当然也有个疑惑就是在调用fastRemove(int index)方法时,为什么要使用System.arraycopy()方法。