Java 正确循环遍历 List 删除

74 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

前言

以前老早就学习过如何在循环遍历 List 的情况下操作(新增/删除)。现在在项目中又碰到了一次,所以在此记录一下,以免有些遗忘,还得去 Google 。直接翻自己的博客就行了。

快速失败 vs 安全失败

快速失败(fail-fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),就会抛出ConcurrentModificationException。

原理

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量。集合在遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返遍历;否则抛出异常,终止遍历。 Tip:这里异常的抛出条件是检测到 modCount=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。「比如,删除成功后,数据和你想要的最终结果不一致,但是没报错;还有一种是你原本的数据就刚好,你当前的数据跑完代码是正确的,但是其他数据就不一定了」

场景:

java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

安全失败(fail-safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

原理

优势:迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对源集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。 缺点:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景

java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用。

实战

ArrayList

**背景:**我是使用 fastjson 进行 json 串整合的时候碰到的,但是 JSONArray 使用的是 ArrayList; 「JSONObject 底层使用 HashMap 的删除函数」。

增强 for 循环

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    for (String s:list) {
        if(s=="2"){       // tips:当 s=="3" ,会成功删除元素
            			  // 当等于 list 中其他三个元素时都会报错:ConcurrentModificationException
            list.remove("2");
        }
    }
    log.info("before:"+list);
}

//在操作后 break:马上跳出循环,则不会报错
for (String s:list) {
    if(s=="2"){
        list.remove("2");
        break;
    }
}

普通 for 循环

不会报错,但是取得值可能会出现异常**

这主要是因为删除元素后,被删除元素后的元素索引发生了变化。假设被遍历list中共有10个元素,当 删除了第3个元素后,第4个元素就变成了第3个元素了,第5个就变成 了第4个了,但是程序下一步循环到的索引是第4个, 这时候取到的就是原本的第5个元素了。 比如下面的程序:

public static void main(String[] args) {
        List<String> list=new ArrayList<String>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        list.add("6");
        list.add("7");
        list.add("8");

        for(int i=0;i<list.size();i++){
            if(i%3==0){
                list.remove(i);
            }
        }

    	//逆向的 for 循环可以消除元素移动的影响,打印正常
        //for(int i=list.size()-1;i>=0;i--){
        //    if(i%3==0){
        //        list.remove(i);
        //    }
        //}
        log.info("after:"+list);
    }

打印结果如下:

- after:[2, 3, 4, 6, 7, 8]

按照代码逻辑:本来正确的应该是:- after:[2, 3, 5, 6, 8]

*Iterator 方式

这种方式可以正常删除,建议使用。

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("1");  //
    list.add("2");
    list.add("3");
    list.add("4");  //
    list.add("5");
    list.add("6");
    list.add("7");  //
    list.add("8");

   Iterator<String> iterator=list.iterator();
   while (iterator.hasNext()){
       String s=iterator.next();
       if((Integer.parseInt(s)-1)%3==0){
           iterator.remove();  //***
           //list.remove(s);  这里要使用 Iterator的remove 方法移除当前对象,
           // 如果使用 List 的 remove 方法,则同样会出现 ConcurrentModificationException
       }
   }
    log.info("after:"+list);
}

总结

  1. 增强 for 循环:用 break,可以防止报错
  2. for 循环:用反向(反向遍历)删除可以正常运行
  3. 必须用 Iterator 迭代器自带的 remove 函数。

源码解析

ArrayList.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;
}
src      the source array.
srcPos   starting position in the source array.
dest     the destination array.
destPos  starting position in the destination data.
length   the number of array elements to be copied.
public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos, 			int length);

删除一个元素后,会重新改变 ArrayList 中的 elementData,而这个 elementData 就代表 ArrayList 的值

根据元素删除元素

//也是相当于通过 下标删除元素

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
    }

普通 for 循环

就是按照上面的 remove 方法的源码运行,不会报错!

增强 for 循环

public Iterator<E> iterator() {
    return new Itr();
}

Itr():是 ArrayList 中实现 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;

        Itr() {}
        
        // 这个 hasNext 很关键,当遍历到倒数第二个的时候,cursor刚好等于size
        //所以当 if 条件是倒数第二个的时候,不会报错。
        
        /*
        	返回的是:增强的 for()括号中的值
        */
        public boolean hasNext() {
            return cursor != size;
        }
        
        // 每运行一次 next() 就会 cursor + 1
         @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];
        }
        
        //当两者不等时,
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

        
		......

/*
在第一次初始化的时候会把 modCount 赋值给 expectedModCount;进行增加或者删除,modCount(记录改变次数的)都会加一,所以两者不相等时,会报错异常 ConcurrentModificationException
*/

迭代器

//在 ArrayList 内部实现类中
/*
调用 ArrayList Itr 内部类的的 remove 方法
*/
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();
    }
}