java 集合类如何正确在迭代中添加删除元素

刚开始尝试写博客不久,今天开创一个新的内容方向java,这是我的第一篇关于java的博客。
最近在完成java作业时,遇到了一个使用迭代器的问题。
问题还原
遇到问题的代码过于复杂,我这里写了一个非常简单的反例,足以还原问题的原貌。

import java.util.*;
public class test{
    public static void main(String[] args){
        List number = new LinkedList<>();
        number.addAll(Arrays.asList(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
        for (Integer i : number) {
            if (i > 5)
                number.remove(i);
        }
        System.out.println(number.toString());
    }
}

这段代码目的是去掉ArrayList 类型的: number中大于5的数字
报错结果是这样的

Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:966)
        at java.util.LinkedList$ListItr.next(LinkedList.java:888)
        at test.main(test.java:6)

出现了这个 java.util.ConcurrentModificationException的异常。

原因分析:
上面代码中我使用了Iterator迭代器,从而可以使用foreach语法实现遍历。Iterator是一个接口,Itr是一个实现这个接口的类,为了探究原因,最好查看一下迭代器的源码,在IDEA下,按住ctrl点击Itr,就看到了源码。
注意到在源码中,Itr的开头有一句

int expectedModCount = modCount;

也就是在类创建时,就将modCount赋值给了expectedModCount
然后在迭代器开始迭代,即调用next()这个函数的时候,都会调用一次内部的checkForComodification()函数
这个函数我摘录出来,如下:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

这里就得到了ConcurrentModificationException这个异常的来源,是因为modCount和expectedModCount不相等。
modCount是原来集合的版本号,每次修改(增、删)集合都会加1;expectedModCount是当前集合的版本号,当我在迭代过程中,删除了一个元素,原来集合的modCount就+1,而Itr迭代器实例是在迭代开始时创立的,expectedModCount并不会随着迭代过程发生改变,所以就会出现两个值不想等的情况。
除此之外,java这样检查的原因在于一个叫快速失败机制(fail-fast)
快速失败机制产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过Iterator遍历集合时,该集合的内容被其他线程所改变,则会抛出ConcurrentModificationException异常。
所以java这样检查为的是多线程数据处理的安全性。

解决问题
由于在外部LinkedList本身删除或添加一个元素,在迭代器内部的expectedModCount没有跟着变化,所以就要使用迭代器自己提供的remove和add函数,这样迭代的expectedModCount就会同步变化,就不会报错。
修改后的代码如下:

import java.util.*;
public class test{
    public static void main(String[] args){
        List number = new LinkedList<>();
        number.addAll(Arrays.asList(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
        Iterator iterator = number.iterator();
        while(iterator.hasNext()) { // 判断是否还有下一个元素
            int i = iterator.next();
            if (i > 5)
                // 注意这里使用的是迭代器对象 iterator的remove,不是number的remove
                iterator.remove();
        }
        System.out.println(number.toString());
    }
}

同时,在java8中加入了函数式编程的一些语法,所以我用lamada表达式配合removeif函数来实现以上功能时,代码量会大大减小,这里一起分享一下:

import java.util.*;
public class test{
    public static void main(String[] args){
        List number = new LinkedList<>();
        number.addAll(Arrays.asList(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
        number.removeIf(num->num>5);  // 这里的遍历和删除只有一短行就实现了
        System.out.println(number.toString());
    }
}

具体的lamda表达式生动解释,参见这篇文章。

补充一点:上面的例子之所以用LinkedList,而不用List接口的另一个实现ArrayList类,是因为ArrayList的内部实现是依赖于数组的,数组本身不方便插入和删除,所以ArrayList类在实现接口函数remove时,直接抛出UnsupportedOperationException异常,也就是它不支持插入和删除,相比之下,LinkedList类是基于链表数据结构的实现,对于插入和删除就非常的简单和高效了。

你可能感兴趣的:(java)