java集合类遍历的同时如何进行删除操作

java集合类遍历的同时进行删除操作

1. 背景

在使用java的集合类遍历数据的时候,在某些情况下可能需要对某些数据进行删除。往往操作不当,便会抛出一个ConcurrentModificationException,本方简单说明一下错误的示例,以及一些正确的操作并简单的分析下原因。

P.S. 示例代码和分析是针对List的实例类ArrayList,其它集合类可以作个参考。

2. 代码示例

示例代码如下,可以根据注释来说明哪种操作是正确的:

public class TestIterator {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        print(list);
 
        // 操作1:错误示范,不触发ConcurrentModificationException
        System.out.println("NO.1");
        List list1 = new ArrayList<>(list);
        for(String str:list1) {
            if ("4".equals(str)) {
                list1.remove(str);
            }
        }
        print(list1);
        // 操作2:错误示范,使用for each触发ConcurrentModificationException
        System.out.println("NO.2");
        try{
            List list2 = new ArrayList<>(list);
            for(String str:list2) {
                if ("2".equals(str)) {
                    list2.remove(str);
                }
            }
            print(list1);
        }catch (Exception e) {
            e.printStackTrace();
        }
        // 操作3:错误示范,使用iterator触发ConcurrentModificationException
        try{
            System.out.println("NO.3");
            List list3 = new ArrayList<>();
            Iterator iterator3 = list3.iterator();
            while (iterator3.hasNext()) {
                String str = iterator3.next();
                if ("2".equals(str)) {
                    list3.remove(str);
                }
            }
            print(list3);
        }catch (Exception e){
            e.printStackTrace();
        }
        // 操作4: 正确操作
        System.out.println("NO.4");
        List list4 = new ArrayList<>(list);
        for(int i = 0; i < list4.size(); i++) {
            if ("2".equals(list4.get(i))) {
                list4.remove(i);
                i--; // 应当有此操作
            }
        }
        print(list4);
        // 操作5: 正确操作
        System.out.println("NO.5");
        List list5 = new ArrayList<>(list);
        Iterator iterator = list5.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if ("2".equals(str)) {
                iterator.remove();
            }
        }
        print(list5);
 
    }
 
    public static void print(List list) {
        for (String str : list) {
            System.out.println(str);
        }
    }
}

P.S. 上面的示例代码中,操作1、2、3都是不正确的操作,在遍历的同时进行删除,操作4、5能达到预期效果,推建使用第5种写法。

3. 分析

首先,需要先声明3个东东:

  • 1. for each底层采用的也是迭代器的方式(这个我并没有验证,是查找相关资料得知的),所以对for each的操作,我们只需要关注迭代器方式的实现即可。
  • 2. AraayList底层是采用数组进行存储的,所以操作4实现是不同于其它(1、2、3、5)操作的,他们用的都是迭代器方式。
  • 3. 鉴于1、2点,其实本文重点关注的是采用迭代器的remove(操作5)为什么没有问题,而采用集合的remove(操作1、2、3)就不行。

3.1 为什么操作4没有问题

        // 操作4: 正确操作
        System.out.println("NO.4");
        List list4 = new ArrayList<>(list);
        for(int i = 0; i < list4.size(); i++) {
            if ("2".equals(list4.get(i))) {
                list4.remove(i);
                i--; // 应当有此操作
            }
        }

这个其实没什么太多说的,ArrayList底层采用数组,它删除某个位置的数据实际上就是把从这个位置下一位开始到最后位置的数据在数组里整体前移一位(基本知识,不多说明)。所以在遍历的时候,重点其实是索引值的大小,底层实现是需要依赖这个索引 的,这也是为什么最后有个i--,因为我们删除2的时候,索引值i为1,删除的时候,就把索引为2到list.size()-1的数据都前移一位,如果不把i-1,那么下一轮循环时,i的值就为2,这样就把原来索引值为2,而现在索引值为1的数据给漏掉了,这个地方需要注意一下。比如,如果原数据中索引1、2的数据都为2,想把2都删除掉,如果不进行i--,那么把索引1处的2删除掉后,下一次循环判断时,就会把原来索引为2,现在索引为1的这个2给遗漏掉了。

3.2 采用迭代时ConcurrentModificationException产生的原因

其实这个异常是在迭代器的next()方法体调用checkForComodification()时抛出来的:

看下迭代器的这两个方法的源码:

       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() {
            // 问题就在modCount与expectedModCount的值
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

1. 首先说明,modCount这个值是ArrayList的一个变量,而expectedModCount是迭代器的一个变量。

modCount:该值是在集合的结构发生改变时(如增加、删除等)进行一个自增操作,其实在ArrayList中,只有删除元素时这个值才发生改变。

expectedModCount:该值在调用集合的iterator()方法实例化迭代器的时候,会将modCount的值赋值给迭代器的变量 expectedModCount。也就是说,在该迭代器的迭代操作期间,expectedModCount的值在初始化之后便不会进行改变,而modCount的值却可能改变(比如进行了删除操作),这也是每次调用next()方法的时候,为什么要比较下这两个值是否一致。

其实,我是把它们看作类似于CAS理论的实现来理解的,其实在迭代器遍历的时候调用集合的remove方法,代码上看起来是串行的,但是可以认为是两个不同线程的并行操作这个ArrayList对象(我也是看了下其它资料,才试着这样去理解)。

3.3 为什么在遍历时使用迭代器的remove没有问题

依据3.2条,我们知道,既然使用ArrayList的remove方法出现ConcurrentModificationException的原因在于modCount与expectedCount的值,那么问题就很明晰了,先看下迭代器的remove方法的源码:

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            // 虽然这里也调用了这方法,但是本次我们可以先忽略,因为这个remove()方法是
            //iterator自已的,也就是可以看作遍历和删除是串行发生的,目前我们尚未开始进行移除
            //操作,所以这里的校验不应该抛出异常,如果抛出了ConcurrentModificationException,
            //那只能是其它线程改了当前集合的结构导致的,并不是因为我们本次尚未开始的移除操作
            checkForComodification();
 
            try {
                // 这里开始进行移除
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                // 重新赋值,使用expectedModCount与modCount的值保持一致
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
     }

注意看下我的注释,在调用迭代器的remove方法时,虽然也是在调用集合的remove方法 ,但是因为这里保持了modCount与expectedModCount的数据一致性,所以在下次调用next()方法,调用checkForComodification方法时,也就不会抛出ConcurrentModificationException了。

3.4 为什么操作1没有抛出ConcurrentModificationException

其实操作1虽然使用for each但是上面说过,其实底层依然是迭代器的方式,这既然是迭代器,然而采用集合的remove方法,却没有抛出ConcurrentModificationException, 这是因为移除的元素是倒数第二个元素的原因。

迭代器迭代的时候,调用hasNext()方法来判断是否结束迭代,若没有结束,才开始调用next()方法,获取下一个元素,在调用next()方法的时候,因为调用 checkForComodification方法时抛出了ConcurrentModificationException。

所以,如果在调用hasNext()方法之后结束循环,不调用next()方法,就不会发生后面的一系列操作了。

既然还有最后一个元素,为什么会结束循环,问题就在于hasNext()方法,看下源码:

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

其实每次调用next()方法迭代的时候,cursor都会加1,cursor就相当于一个游标,当它不等于集合大小size的时候,就会一直循环下去,但是因为操作1移除了一个元素,导致集合的size减一,导致在调用hasNext()方法,结束了循环,不会遍历最后一个元素,也就不会有后面的问题了。

java集合中的一个移除数据陷阱

遍历集合自身并同时删除被遍历数据

使用Set集合时:遍历集合自身并同时删除被遍历到的数据发生异常

Iterator it = atomSet.iterator(); 
  while (it.hasNext()) {  
   if (!vars.contains(it.next())) {
    atomSet.remove(it.next());
   }
  } 

抛出异常:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$KeyIterator.next(HashMap.java:828)
at test2.Test1.main(Test1.java:16)

异常本质原因

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationEx ception 异常。

所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

解决

使用Iterator的remove方法

Iterator it = atomVars.iterator();  
   while (it.hasNext()) {  
    if (!vars.contains(it.next())) {
     it.remove();
    }
   } 

代码能够正常执行。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

你可能感兴趣的:(java集合类遍历的同时如何进行删除操作)