纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

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

NetWhite   2021-09-14 我要评论
想了解java集合类遍历的同时怎样进行删除操作的相关内容吗NetWhite在本文为您仔细讲解java集合类遍历进行删除的相关知识和一些Code实例欢迎阅读和指正我们先划重点:java集合类遍历,集合类遍历删除,java遍历集合下面大家一起来学习吧

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

1. 背景

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

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

2. 代码示例

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

public class TestIterator {
    public static void main(String[] args) {
        List<String> 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<String> 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<String> 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<String> list3 = new ArrayList<>();
            Iterator<String> 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<String> 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<String> list5 = new ArrayList<>(list);
        Iterator<String> iterator = list5.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if ("2".equals(str)) {
                iterator.remove();
            }
        }
        print(list5);
 
    }
 
    public static void print(List<String> 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<String> 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<String> 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<String> it = atomVars.iterator();  
   while (it.hasNext()) {  
    if (!vars.contains(it.next())) {
     it.remove();
    }
   } 

代码能够正常执行

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


相关文章

猜您喜欢

  • Java 线程创建 Java线程创建的四种方式总结

    想了解Java线程创建的四种方式总结的相关内容吗威斯布鲁克.猩猩在本文为您仔细讲解Java 线程创建的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Java,线程创建,Java,线程下面大家一起来学习吧..
  • Spring Boot整合Spring Security Java SpringBoot安全框架整合Spring Security详解

    想了解Java SpringBoot安全框架整合Spring Security详解的相关内容吗DrLai在本文为您仔细讲解Spring Boot整合Spring Security的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Spring,Boot,Spring,Security下面大家一起来学习吧..

网友评论

Copyright 2020 www.sopisoft.net 【绿软下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式