Bootstrap

关于for循环删除集合元素的几个坑


1.原始的for循环删除集合元素的时候有可能会漏掉某个元素

测试代码:

/**
* 假设需要删除集合中的所有的"A"元素
* 如果使用原始的for循环,那么就可能会漏删
*/
List<String> list = new ArrayList<>();
list.add("A");list.add("A");list.add("B");
for(int i=0; i<list.size(); i++) {
if("A".equals(list.get(i))) {list.remove(i);
}
}
System.out.println(list);

 
 

输出结果:
[A, B]
可以看到集合中只有一个"A"被删了,第二个"A"却还在。
在这里插入图片描述

其实解决办法也很简单,在删除元素之后再加一行代码i--就解决了

2.增强型for循环删除集合元素会报出ConcurrentModificationException异常

修改一下上面的测试代码

List<String> list = new ArrayList<>();
		list.add("A");
		list.add("A");
		list.add("B");
		for(String item : list) {
			if("A".equals(item)) {
				list.remove(item);
			}
		}
		System.out.println(list);

 
 

打印结果是ConcurrentModificationException异常。
增强型for循环其实是编译器认可的语法,在到了虚拟机那边只认迭代器循环,所以在编译为.class文件的时候,最终还是变成了迭代器循环。通过反编译得到如下代码:

List<String> list = new ArrayList();
        list.add("A");
        list.add("A");
        list.add("B");
        Iterator var3 = list.iterator();
while(var3.hasNext()) {
            String item = (String)var3.next();
            if ("A".equals(item)) {
                list.remove(item);
            }
        }

 
 

迭代器这块比较复杂,我们从源码上慢慢探索。
list.iterator()其实际上是获得了一个内部类Itr对象,这个类实现了Iterator接口。循环所用到的hasnext(),next()方法都是在这个类中实现的。
以下代码来自openJDK1.8的AbstractList类中
在这里插入图片描述
以下代码来自ArrayList
在这里插入图片描述

大概理一下抛出异常的思路:
迭代器循环每次调用next方法,next方法中第一步会先调用checkForComodification方法,checkForComodification方法中会校验modCountexpectedModCount两个变量是否一样(我把这两个变量理解为版本号),如果不一样就会抛出ConcurrentModificationException异常。
modCount是集合的成员变量,初始值为1,当创建迭代器对象的时候,modCount会赋值给expectedModCount。当调用了list本身的remove方法的时候,会让expectedModCount自增,却不改变modCount。于是到了下一次迭代器的next方法去取集合元素的时候会先执行checkForComodification()方法判断版本号是否一致,这时候集合的版本号和迭代器的版本号不一致就抛出了异常。
而如果是调用迭代器的remove方法,它会先调用集合的remove方法,然后执行cursor- -,再把modCount赋值给expectedModCount。这样一来既保证了下一次迭代的时候不会漏过任何元素,也避免了next方法校对版本号的时候抛出异常

3.在循环中删除集合元素的正确方法

在循环中删除元素的正确方法是用迭代器的remove方法

List<String> list = new ArrayList<>();
        list.add("A");
        list.add("A");
        list.add("B");
        for(Iterator<String> i = list.iterator(); i.hasNext(); ) {
            String item = i.next();
            if ("A".equals(item)) {
            	//这里调用迭代器的rermove方法而不是集合的remove方法
                i.remove();
            }
        }
        System.out.println(list);

 
 

也许你会觉得上面的写法太麻烦,“我只是想删除集合中的所有A元素,有没有更优雅的写法呢”。答案是有的,在Java8以后,我们就可以用一行代码来完美代替上面的写法了:

list.removeIf(item -> "A".equals(item));

 
 

实际上removeIf方法内部也是用了迭代器循环
在这里插入图片描述

4.为什么要设计“快速报错”机制

这是一种集合的保护机制,为了防止多个线程同时修改同一个集合的内容。我们先假设没有快速失败机制,当一条线程正在迭代遍历某个集合的过程中,另一个线程介入其中,并且删除了集合内的某个对象,那么就出问题了:就像上面说过的一样,元素被删除后,集合缩容,第一个线程要进行下一次迭代时,就会漏掉一个元素。这种数据不同步所带来的问题可能再当前步骤下不会出现异常,直到在后续的某个代码环节中才会暴露出问题,从而给我们排查定位异常带来难度。
在这里插入图片描述
而有了快速失败机制后,一个线程的删除了集合中的某个元素,集合的modCount+1,另一个集合调用迭代器的next方法,发现版本号不一致,于是就直接抛出异常,终止迭代,让我们能够快速的定位到问题所发生的时间和位置。

ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的机制。这个话题我们下次再研究吧。

附上我自己遍历删除List的方法

直接采用正常的for进行遍历,但是使用倒序进行遍历,这样就不会遇到集合下标产生的问题~

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList();
        list.add("_");
        list.add("_");
        list.add("+");
        list.add("_");
        list.add("_");
        
        System.out.println(list);

        for (int i = list.size() - 1; i >= 0 ;i --) {
            if(list.get(i) == "_"){
                list.remove(i);
            }
        }
        System.out.println(list);

    }

原文链接:https://blog.csdn.net/czx2018/article/details/84770921

;