ArrayList遍历时删除元素

ArrayList作为集合,一般有三种遍历方式,分别是普通for遍历,增强for遍历(foreach遍历)和迭代器遍历,采用不同的遍历方式时,有的方式可以删除元素,有的则不可以,首先结论先行:
1.for循环,可以删除元素
2.foreach循环,不可以删除元素
3.迭代器循环删除,调用iterator.remove()可以,使用list.remove()不可以
接下来分析不同方式的出现不同结果的原因:

1.使用foreach遍历

先贴代码样例:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        for(String str:list){
            System.out.println(str);
            if(str.equals("aa")){
                list.remove("aa");
            }
        }
        System.out.println(list);
    }

执行结果为:

typescript array删除数组元素 arraylist foreach删除_删除元素

这里有有一个很有意思的现象,就是遍历删除倒数第二个元素的时候,是可以成功删除的,并不会报错,删除其余位置的元素都会报错,代码和结果如下:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        for(String str:list){
            System.out.println(str);
            if(str.equals("cc")){
                list.remove("cc");
            }
        }
        System.out.println(list);
    }

typescript array删除数组元素 arraylist foreach删除_System_02


是不是感觉很神奇,这个出现的原因后面分析

2.迭代器遍历

先贴代码样例:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String str = iterator.next();
            System.out.println(str);
            if("aa".equals(str)){
                iterator.remove();
            }
        }
        System.out.println(list);
    }

执行结果为

typescript array删除数组元素 arraylist foreach删除_删除元素_03


这里有一个注意的点是必须使用迭代器的remove()方法,不能使用list.remove()方法,否则会抛出错误,原因同样后面分析,看一下代码样例和执行结果

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String str = iterator.next();
            System.out.println(str);
            if("aa".equals(str)){
                list.remove("aa");
            }
        }
        System.out.println(list);
    }

typescript array删除数组元素 arraylist foreach删除_java_04

3.普通for查询遍历

老规矩,先来代码样例

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        for(int i =0;i<list.size();i++){
            System.out.println(list.get(i));
            if("dd".equals(list.get(i))){
                list.remove("dd");
            }
        }
        System.out.println(list);
    }

执行结果为

typescript array删除数组元素 arraylist foreach删除_删除元素_05

4.分析原因

首先,先看一下ArrayList的继承关系图

typescript array删除数组元素 arraylist foreach删除_java_06


首先继承了AbstractList类,在这个类中有一个全局变量modCount,这个变量是用来记录当list中元素变化时(新增和删除)的操作次数,当Arraylist使用foreach遍历时,会根据集合对象创建一个iterator迭代对象(在ArrayList中是一个Itr的内部类),用这个迭代对象来遍历集合,而使用迭代器遍历时主要会用到两个方法hasNext()和next()方法,ltr内部类源码如下

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;

        // prevent creating a synthetic constructor
        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        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];
        }

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

当迭代器遍历时,会首先调用hasNext方法,判断是否有下一个,如果为true则会调用next()方法,使用next方法首先会校验modCount和expectedModCount是否相等,如果不相等就会抛出上面执行结果的中的异常,那么为什么不相等呢,我们看一下ArrayList的remove(Obejct o)方法

public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        found: {
            if (o == null) {
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
        //调用快速删除方法
        fastRemove(es, i);
        return true;
    }

在这个方法中会调用fastRemove()方法,源码如下

private void fastRemove(Object[] es, int i) {
		//每调用一次mmodCount就会自增一次
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }

我们发现每调用一次remove()方法,modCount就会自增,而expectedModCount 默认和modCount相等,当modCount自增加1,而expectedModCount 没有增加,就会抛出异常,这也就是foreach遍历时抛出的错误的原因,使用迭代器遍历,使用list.remove()抛出错误也是同样的原因,那么为什么使用foreach删除第二个元素时就能成功呢,这是因为hasNext()方法返回的结果为false,迭代器认为没有下一个元素了,就不会执行next(),就不会进行modCount和expectedModCount 的判断,上面的执行结果也能看到,当删除倒数第二个元素时,最后一个元素并没有遍历打印,这又是为什么呢?因为hasNext()方法中cursor变量的值和此时的size值相等。
cursor类似于一个游标或者指针,每进行一次遍历时就会+1,开始默认为0,以上面的代码实例,遍历第一次时,cursor=0,size=4,末尾cursor+1变成1;第二次时cursor=1,size=4,末尾cursor+1变成2;第三次时cursor=2,size=4,末尾cursor+1变成3,此时因为调用了list.remove()所以此时size-1变成了3;第四次时cursor=3,size=3,cursor=size,便不会获取最后一个元素,也就不会校验和抛出异常。
那么又为什么迭代器遍历时调用iterator.remove()不会报错呢?这是因为在iterator.remove()时会同步修改expectedModCount的值,使其和modCount的值一致,就会通过校验,不会抛出错误
使用普通for循环时,因为没有expectedModCount和modCount的比较,也就不会抛出异常

结尾

所以结论就是开始的结论,需要注意的事,前面提到过新增也会使modCount变化,而迭代器每一次遍历都会校验modCount和expectedModCount 是否一致,所以使用foreach遍历使,是不能够调用list.add()新增的,也会抛出同样的错误,这里贴一下样例和实验结果,感兴趣的朋友可以尝试一下,以上观点属于个人见解,如有问题,欢迎讨论

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("aa");
        list.add("bb");
        list.add("cc");
        list.add("dd");
        for(String str:list){
            System.out.println(str);
            list.add("ee");
        }
        System.out.println(list);
    }

typescript array删除数组元素 arraylist foreach删除_java_07