刚开始尝试写博客不久,今天开创一个新的内容方向java,这是我的第一篇关于java的博客。
最近在完成java作业时,遇到了一个使用迭代器的问题。
问题还原:
遇到问题的代码过于复杂,我这里写了一个非常简单的反例,足以还原问题的原貌。
import java.util.*;
public class test{
public static void main(String[] args){
List<Integer> 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<Integer> number = new LinkedList<>();
number.addAll(Arrays.asList(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
Iterator<Integer> 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<Integer> 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类是基于链表数据结构的实现,对于插入和删除就非常的简单和高效了。