ArrayList循环遍历删除元素出现问题

  • 1. 第一种循环删除出现异常
  • 2. 第二种循环删除不报异常,但是会出现有些数据没有删除的情况
  • 3. 总结


1. 第一种循环删除出现异常

import java.util.ArrayList;

/**
 * @author LanceQ
 * @version 1.0
 * @time 2021/4/28 19:16
 */
public class ListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for(String l:list){
            System.out.println(list.remove(l));//并发修改异常
        }

    }
}

循环删除第二个开始爆并发异常

true
Exception in thread "main" java.util.ConcurrentModificationException
  • 先了解一下remove方法,其方法有两个,一个是下面这个通过对象删除,一个是通过索引删除。
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;
    }
  • 一般情况下程序的执行路径会走到else路径下,最终调用faseRemove方法:
private void fastRemove(int index) {
        modCount++;   //AbstractList中的变量
        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
    }
  • 其上面错误产生的原因:其实foreach写法是对实际的Iterable、hasNext、next方法的简写,问题在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):
protected transient int modCount = 0;

        public E next() {
            checkForComodification();<--------这里
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)      <----判断
                throw new ConcurrentModificationException();<--------这里异常
        }

        int expectedModCount = modCount;

这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。

import java.util.ArrayList;
import java.util.Iterator;

/**
 * @author LanceQ
 * @version 1.0
 * @time 2021/4/28 19:16
 */
public class ListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        //错误的删除方法
//        for(String l:list){
//            System.out.println(list.remove(l));//并发修改异常
//        }
        System.out.println(list);
        Iterator<String> it = list.iterator();
        //这里判断的是下一个元素是否存在
        while (it.hasNext()){
            System.out.println(it.hasNext());
            //需要进行next操作才可以进行remove,
            // 否则会出现Java.lang.IllegalStateException异常(非法状态异常)
            it.next();
            it.remove();
        }
        System.out.println(list);
    }
}
运行结果
[1, 2, 3]
true
true
true
[]
  • Java.lang.IllegalStateException异常(非法状态异常)出现的原因是 删除了一个不满足条件的元素。通过Iterator来删除,首先需要使用next方法迭代出集合中的元素,然后才能调用remove方法,否则集合可能抛出java.lang.IllegalStateException异常。
  • 注意remove对象是否存在,如果这个记录已被remove掉,再次remove会出现此异常,容易出现在对同一对象(如List)做多次迭代remove的情景中。

2. 第二种循环删除不报异常,但是会出现有些数据没有删除的情况

import java.util.ArrayList;
import java.util.Iterator;

/**
 * @author LanceQ
 * @version 1.0
 * @time 2021/4/28 19:16
 */
public class ListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        remove2(list);
    }

    private static void remove2(ArrayList<String> list) {
        System.out.println(list);
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.remove(i));
        }
        System.out.println(list);
    }

}
运行结果
[1, 2, 3, 4, 5]
1
3
5
[2, 4]

这次的remove调用的是另一个remove方法

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

        modCount++;
        E oldValue = elementData(index);

        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

        return oldValue;
    }
  • 可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。
  • 针对上方出现的运行结果与预想不一致,是由于在遍历第一个字符串时,因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时,后一个字符串并没有遍历到,所以无法删除。

针对这种情况可以倒序删除的方式来避免:

private static void remove2(ArrayList<String> list) {
        System.out.println(list);
        for (int i = list.size() - 1; i >= 0; i--) {
            System.out.println(list.remove(i));
        }
        System.out.println(list);
    }
[1, 2, 3, 4, 5]
5
4
3
2
1
[]

3. 总结

  • 通过foreach方式进行删除的modCount变量的改变,会出现非法状态异常,可通过iterator迭代器的方式进行判断,删除。
  • 通过for循环变量list的长度,正序来进行list中元素的移除,会出现漏删除的情况,可通过倒序删除的方式来解决。