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();
    }
   } 

代码能够正常执行。

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

时间: 2021-09-14

解决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利用遍历进行删除操作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或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使用entrySet方法获取Map集合中的元素

本文为大家分享了使用entrySet方法获取Map集合中元素的具体代码,供大家参考,具体内容如下 /*--------------------------------- 使用entrySet方法取出Map集合中的元素: ....该方法是将Map集合中key与value的关系存入到了Set集合中,这个关系的数据类型是Map.Entry ....entrySet方法返回值类型的具体写法为:Set< Map.Entry<KeyType , ValueType> > -----------

Java使用keySet方法获取Map集合中的元素

本文为大家分享了Map集合中利用keySet方法获取所有的元素值,供大家参考,具体内容如下 /*--------------------------- Map集合中利用keySet方法获取所有的元素值: ....keySet方法:将Map中的所有key值存入到Set集合中, ....利用Set集合提供的迭代器获取到每一个key值,再通过key值获得相应的value值 ----------------------------*/ package pack03; import java.util.*

java 键盘输入一个数,输出数组中指定元素的示例

如下所示: package com.lcn.day05; import java.util.Scanner; public class ArrayDemo7 { /** *键盘输入一个数,输出数组中指定元素 */ public static void main(String[] args) { // 定义一个数组 int[] array = new int[]{123,456,789,321,654,987}; //创建输入对象 Scanner sc = new Scanner(System.i

Java封装数组实现包含、搜索和删除元素操作详解

本文实例讲述了Java封装数组实现包含.搜索和删除元素操作.分享给大家供大家参考,具体如下: 前言:在上一小节中我们已经会了如何获取和如何修改数组中的元素,在本小节中我们将继续学习如何判断某个元素是否在数组中存在.查询出某个元素在数组中的位置.以及删除数组中元素等方法的编写. 1.查找数组中是否包含元素e,返回true或false //查找数组中是否包含元素e public boolean contains(int e) { for (int i = 0; i < size; i++) { if

python实现从字典中删除元素的方法

本文实例讲述了python实现从字典中删除元素的方法.分享给大家供大家参考.具体分析如下: python的字典可以通过del方法进行元素删除,下面的代码详细演示了这一过程 # Create an empty dictionary d = {} # Add an item d["name"] = "Fido" assert d.has_key("name") # Delete the item del d["name"] ass

PHP从数组中删除元素的四种方法实例

茴香豆的"茴"字有四种写法,PHP从数组中删除元素也有四种方法 ^_^. 删除一个元素,且保持原有索引不变 使用 unset 函数,示例如下: <?php $array = array(0 => "a", 1 => "b", 2 => "c"); unset($array[1]); //↑ 你想删除的key ?> 输出: Array (     [0] => a     [2] =>

php数组中删除元素之重新索引的方法

如果要在某个数组中删除一个元素,可以直接用的unset,但今天看到的东西却让我大吃一惊 复制代码 代码如下: <?php $arr = array('a','b','c','d'); unset($arr[1]); print_r($arr); ?> print_r($arr)之后,结果却不是那样的,最终结果是 Array ( [0] => a [2] => c [3] => d ) 那么怎么才能做到缺少的元素会被填补并且数组会被重新索引呢?答案是 array_splice(

Java函数式编程(四):在集合中查找元素

查找元素 现在我们对这个设计优雅的转化集合的方法已经不陌生了,但它对查找元素却也是无能为力.不过filter方法却是为这个而生的. 我们现在要从一个名字列表中,取出那些以N开头的名字.当然可能一个也没有,结果可能是个空集合.我们先用老方法实现一把. 复制代码 代码如下: final List<String> startsWithN = new ArrayList<String>(); for(String name : friends) { if(name.startsWith(&

php数组中删除元素的实现代码

复制代码 代码如下: <?php $arr = array('a','b','c','d'); unset($arr[1]); print_r($arr); ?> print_r($arr)之后,结果却不是那样的,最终结果是 Array ( [0] => a [2] => c [3] => d 那么怎么才能做到缺少的元素会被填补并且数组会被重新索引呢?答案是array_splice(): 复制代码 代码如下: <?php $arr = array('a','b','c'