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

目录
  • java集合类遍历的同时进行删除操作
    • 1. 背景
    • 2. 代码示例
    • 3. 分析
  • 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();
    }
   } 

代码能够正常执行。

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

(0)

相关推荐

  • Java list利用遍历进行删除操作3种方法解析

    这篇文章主要介绍了Java list利用遍历进行删除操作3种方法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Java三种遍历如何进行list的便利删除: 1.for循环: 常见初五写法:(由于下标问题达不到想要效果) for(int i=0;i<list.size();i++){ if(list.get(i).equals("del")) list.remove(i); } 应该改为:(倒序操作避免下标问题) int s

  • Java集合使用 Iterator 删除元素

    这篇文章主要介绍了Java集合使用 Iterator 删除元素,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 针对常见的数据集合,比如 ArrayList 列表,对其进行遍历,删除其中符合条件的某个元素,使用 iterator 迭代器进行迭代,代码如下: public class PracticeController { public static void main(String[] args) { List<String> list =

  • Java中List遍历删除元素remove()的方法

    今天碰见根据条件进行list遍历remove的问题,第一时间就是简单for循环remove,只知道这么写不行,不安全,可是为什么呢?你想过吗?下面就关于List遍历remove的问题,深挖一下! 一.几种常见的遍历方式 1.普通for循环 2.高级for循环 3.iterator和removeIf 4.stream() 5.复制 6.普通for循环 --> 倒序方式 二.源码篇 1.普通for循环出错原因 public boolean remove(Object o) { if (o == nu

  • 解决JAVA遍历List集合,删除数据时出现的问题

    一.问题描述 有时候,我们会遇到在遍历List集合的过程中删除数据的情况. 看着自己写的代码,感觉完全没有问题,但就是达不到预期的效果,这是为什么呢?下面我们来分析下 String str1 = new String("1"); String str2 = new String("2"); String str3 = new String("3"); String str4 = new String("4"); String

  • Java如何在List或Map遍历过程中删除元素

    遍历删除List或Map中的元素有很多种方法,当运用不当的时候就会产生问题.下面通过这篇文章来再学习学习吧. 一.List遍历过程中删除元素 使用索引下标遍历的方式 示例:删除列表中的2 public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(2); list.add(3); list.add(

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

    目录 java集合类遍历的同时进行删除操作 1. 背景 2. 代码示例 3. 分析 java集合中的一个移除数据陷阱 遍历集合自身并同时删除被遍历数据 异常本质原因 解决 java集合类遍历的同时进行删除操作 1. 背景 在使用java的集合类遍历数据的时候,在某些情况下可能需要对某些数据进行删除.往往操作不当,便会抛出一个ConcurrentModificationException,本方简单说明一下错误的示例,以及一些正确的操作并简单的分析下原因. P.S. 示例代码和分析是针对List的实

  • java集合类arraylist循环中删除特定元素的方法

    在项目开发中,我们可能往往需要动态的删除ArrayList中的一些元素. 一种错误的方式: <pre name="code" class="java">for(int i = 0 , len= list.size();i<len;++i){ if(list.get(i)==XXX){ list.remove(i); } } 上面这种方式会抛出如下异常: Exception in thread "main" java.lang.I

  • java集合类源码分析之Set详解

    Set集合与List一样,都是继承自Collection接口,常用的实现类有HashSet和TreeSet.值得注意的是,HashSet是通过HashMap来实现的而TreeSet是通过TreeMap来实现的,所以HashSet和TreeSet都没有自己的数据结构,具体可以归纳如下: •Set集合中的元素不能重复,即元素唯一 •HashSet按元素的哈希值存储,所以是无序的,并且最多允许一个null对象 •TreeSet按元素的大小存储,所以是有序的,并且不允许null对象 •Set集合没有ge

  • 总结Java集合类操作优化经验

    在实际的项目开发中会有很多的对象,如何高效.方便地管理对象,成为影响程序性能与可维护性的重要环节.Java 提供了集合框架来解决此类问题,线性表.链表.哈希表等是常用的数据结构,在进行 Java 开发时,JDK 已经为我们提供了一系列相应的类来实现基本的数据结构,所有类都在 java.util 这个包里,清单1 描述了集合类的关系. 清单 1.集合类之间关系 Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map

  • java集合遍历的几种方式总结及详细比较

    集合类的通用遍历方式, 用迭代器迭代: Iterator it = list.iterator(); while(it.hasNext()) { Object obj = it.next(); } Map遍历方式: 1.通过获取所有的key按照key来遍历 //Set<Integer> set = map.keySet(); //得到所有key的集合 for (Integer in : map.keySet()) { String str = map.get(in);//得到每个key多对用v

  • Java集合类知识点总结

    集合:只能存储对象,对象类型可以不一样,长度可变. 常用的接口和类: 1.List接口(有序.可重复):ArrayList类.LinkedList.Vector类 2.Set接口(无序.不能重复):HashSet类.TreeSet类 3.Map接口(键值对.键唯一.值不唯一):HashMap类.Hashtable类.TreeMap类 集合类的循环遍历 1.普通for循环:如 for(int i=0;i<arr.size();i++){-} 2.foreach(增强型for循环):如 for(Ob

  • 详解Java 集合类 List 的那些坑

    现在的一些高级编程语言都会提供各种开箱即用的数据结构的实现,像 Java 编程语言的集合框架中就提供了各种实现,集合类包含 Map 和 Collection 两个大类,其中 Collection 下面的 List 列表是我们经常使用的集合类之一,很多的业务代码都离不开它,今天就来看看 List 列表的一些坑. 第一个坑:Arrays.asList 方法返回的 List 不支持增加.删除操作 例如我们执行以下代码: List<String> strings = Arrays.asList(&qu

  • 详解Java集合类之List篇

    目录 1.集合框架体系 2.Collection接口 3.迭代器 4.List接口 5.ArrayList ArrayList扩容机制 ArrayList使用实例 6.Vector 7.LinkedList 增加元素源码分析 删除元素源码分析 LinkedList使用Demo 8.ArrayList和LinkedList的选择 1.集合框架体系 集合框架被设计成要满足以下几个目标. 该框架必须是高性能的.基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的. 该框架允许不同类型的集合,以类

  • 详解Java集合类之Map篇

    目录 1.Map接口介绍 2.Map接口分析 3.Map接口方法 4.Map遍历方式 1.Map接口介绍 Map用于保存具有映射关系的数据:Key - Value 对于Set,底层其实依然是一个Map,但是Set选择不使用Value,也就是Set的Value值始终是一个常量 Map中的Key和Value可以是任何类型的数据,会封装到HashMap$Node对象中 Map中的Key不能重复,但是Value可以重复,当有相同的Key时,等价与替换操作 2.Map接口分析 存放Map键值对是在Hash

随机推荐