提醒:本篇文章的代码是通过eclipse进行操作的,如有使用Intellij IDEA编译器,操作可能会有所不同;另外,如果不想了解异常发生原因,只是单纯地寻找问题的解决办法,那么可以直接跳到最后的"解决办法"那部分内容;最后,此篇文章涉及内容略多,需要一定的阅读时间,有需要的可以挑重点观看。
使用List集合可以帮助我们更为方便地存储各种类型的数据,List集合具有顺序存储和顺序访问的特点;通过Iterator迭代器可以使我们十分方便地遍历集合,并取出集合中的元素。然而一些初学的小伙伴可能很容易就发现一个问题,通过Iterator迭代器对集合进行遍历确实很方便,但是!在使用Iterator时隐藏着一个风险,就是当创建一个迭代器后,再对集合的元素进行修改后,发现这时已经无法通过刚刚创建的那个迭代器再去遍历集合了。这种情况我们称之为“并发异常”。
可能上面的描述还是让人觉得云里雾里的,下面我们通过具体的代码进行详细的讨论。
创建一个List集合,该集合的数据结构是字符串类型,创建好集合后向集合中依次录入"Hello",“World"两个字符串,并进行遍历输出;随后再录入一个字符串"CSDN”,再次遍历输出集合中的元素。
我们先不着急去写代码,先把题目分析清楚然后再进行检验。题目要求我们要创建一个集合,录入相应数据后,进行遍历输出,注意是遍历输出,而不是直接输出集合,既然需要遍历那么就有两种办法(请看下方注释)。第一种是通过for循环,对集合进行遍历;第二种就是通过迭代器,对集合中的元素进行遍历。第一种方法完成此题目的要求是毫无苦难的,这里不做过多介绍,我们要详细介绍的是第二种方法遍历的过程及在解决此问题过程中会出现的异常,并对异常进行分析,最终给出异常的解决办法。
还有一种解决办法,就是使用列表迭代器,在本篇文章中我们不对列表迭代器做过多讲解,仅考虑使用普通迭代器时的情况和解决办法。
思路(通过Iterator实现)
单从题目的要求来看,分析过程是很简单的,但是这里我们还是要给出大致的思路
思路有了,我们按照这个思路可以写出常规的解题代码。
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class MainTest{ public static void main(String[] args) { // TODO Auto-generated method stub List<String> list = new ArrayList<String>(); list.add("Hello"); list.add("World"); Iterator<String> iter = list.iterator(); while(iter.hasNext()) { String s = iter.next(); System.out.println(s); } list.add("CSDN"); while(iter.hasNext()) { String s = iter.next(); System.out.println(s); } } }
输出
Hello World Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967) at test01/string.MainTest.main(MainTest.java:28)
通过输出我们会发现,程序居然报错了?问题出在哪儿了呢?这里我先告诉大家问题所在,然后再详细分析问题出现的原因。其实问题出在了最后一个while循环里!为什么呢?或许你会有疑问,上边那个while循环也没出错啊,怎么到下边这个就有问题了呢?问题虽然出在第二个while循环里,但关键还是在于这两个while循环之间的那个add()语句。
接下来我们分析为什么会出现这个问题。
友情提示:由于接下来的过程分析得较为复杂繁多,前面四步可以大致看一下,重点看第五步的黑体字部分和第六步。不过如果直接跳到第五步第六步可能看不懂,因为前面的也是铺垫。
1.首先我们注意看一下,当你运行后控制台的输出中,抛出的异常,第一句“Exception in thread “main” java.util.ConcurrentModificationException”意思大概是这里出问题了,再来看控制台输出的最后一句“at test01/string.MainTest.main(MainTest.java:28)”,我们点击括号的内容编译器会跳转到问题的那一行,点击后跳转到第二个while循环中的“String s = it.next();”这一行来了,那么我们现在就知道了,问题确实出在了第二个while循环里的next()语句里。
2.我们再点击控制台输出的倒数第二句括号中内容(ArrayList.java:967),它会自动跳转到next()方法具体定义的位置,并且该方法中的第一行代码 checkForComodification();被编译器点亮,表示问题是在这里,这又是一个方法,我们在点击控制台输出的倒数第三行括号里的内容就可以定位到该方法的位置了。
next()方法的方法体如下
说明:除了通过点击控制台第三行输出的地方可以跳转到next()方法,还可以直接查找到该方法。该方法定义在ArrayList类中的Itr下(ArrayList —> Itr —> next()),即可看到next()方法。
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]; }
checkForComodification()方法体如下
说明:与next()方法相同,checkForComodification()方法也在ArrayList类的Itr下
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); //注意这里 }
3.到这里我们就可以很明显地看到,在checkForComodification()方法里的if判断语句抛出了ConcurrentModificationException()异常。那么现在我们重点关注的就是if语句中判断的内容到底是什么才导致了抛出这个异常。
4.if条件判断的是modCount 与expectedModCount是否相等,如果不相等就会抛出异常。那么modCount和expectedModCount又是什么呢?我们通过跟进查找到这两个数据是在哪儿被定义的(eclipse中双击选中该元素,然后按F3或者右键点击Open Declaration),通过查找定位我们可以看到这两个变量的定义代码如下所示。
modCount
protected transient int modCount = 0;
expectedModCount
通过定位可以看到,expectedModCount变量在一个实现了Iterator接口的类里被定义的,由于这个类中的成员方法过多,且与我们所讲的知识点无关,因此在这里我把这些无关的代码都删掉,留下我们需要的代码。可以看到其实我们前边提到的next()方法和checkForComodification()方法都在这个类里面。
private class Itr implements Iterator<E> { int expectedModCount = modCount; @SuppressWarnings("unchecked") 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() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
5.通过上面的查找结果显示,modCount在抽象列表的类中被定义,且是全局变量,被transient修饰,transient修饰的作用就类似于保证了该变量的安全性,在这里我们暂时不需要明白transient修饰的过多含义。而expectedModCount在接口实现类中被定义,且被初始化为modCount。对于这两个变量的来源我们已经搞清楚了,接下来就是这两个变量的作用意义了。对于modCount变量,我们可以认为这个变量的含义是,集合实际修改的次数;对于expectedModCount变量,可以认为该变量的含义是预期修改变量的次数。
6.在调用next()方法时每次都会调用checkForComodification()方法去检查这两个变量是否相同,因为当一个容器被创建后,它应该是固定不变的,而不能发生变化,否则就会抛出ConcurrentModificationException()异常。我们可以理解为,当使用容器后,该容器指向的集合内的元素便被固定写死了,不能再去更改。一旦发生了更改,比如add(),由于add()方法体内会修改modCount的值,那么当再次调用next()方法时,checkForComodification()方法内判断语句就会判断到这两个变量的值不相等,就会抛出异常!
add()方法体
public boolean add(E e) { modCount++; add(e, elementData, size); return true; }
前面我们已经弄清楚了并发异常的原因,那么要怎么解决这个问题呢?其实在这中情况下,这种异常的出现是无可避免的,但并不是说没有解决办法,解决的办法也很简单,有两种方法,第一种就是我们一开始提到的通过for循环对集合进行遍历,这种方法很简单,也不会出问题;如果有同学说,哎我就想用容器进行遍历怎么办呢?这就要用第二个办法了,方案也很简单,就是重新创建一个新的容器来接收已经发生过变化被修改过的集合,然后按照原来的方法遍历一遍就OK了。
下面我们给出解决问题的代码。虽然这些代码都是已经给出了的,但是还是建议大家自己下去亲自动手敲一下哦,印象会更深刻。
方法一 通过for循环解决
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class MainTest{ public static void main(String[] args) { // TODO Auto-generated method stub List<String> list = new ArrayList<String>(); list.add("Hello"); list.add("World"); Iterator<String> iter = list.iterator(); while(iter.hasNext()) { String s = iter.next(); System.out.println(s); } list.add("CSDN"); System.out.println("通过for循环解决后输出:"); for(int i=0;i<list.size();i++) { String str = list.get(i); System.out.println(str); } } }
输出
Hello World 通过for循环解决后输出: Hello World CSDN
方法二 通过新的容器解决
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class MainTest{ public static void main(String[] args) { // TODO Auto-generated method stub List<String> list = new ArrayList<String>(); list.add("Hello"); list.add("World"); Iterator<String> iter = list.iterator(); while(iter.hasNext()) { String s = iter.next(); System.out.println(s); } list.add("CSDN"); System.out.println("通过新容器解决后输出:"); Iterator<String> newIter = list.iterator(); while(newIter.hasNext()) { String str = newIter.next(); System.out.println(str); } } }
输出
Hello World 通过新容器解决后输出: Hello World CSDN
通过两种解决方法的输出,我们可以看到,都成功地解决了并发异常的问题,到这里问题就完美解决了。
本篇文章的介绍重点在于对于问题分析的那一大块,而问题分析这部分的重点又是迭代器,理解迭代器的特点是解决问题的关键。
可以近似得认为,当创建一个迭代器iterator后,固定了此迭代器对应的集合元素,此时就不能再对集合List进行添加、删除等数据的修改操作了,一旦进行添加、删除等修改操作,如果再次通过容器进行访问集合元素的话,就会抛出异常。如果在添加、删除集合中的元素后仍需要进行读取操作(一般来说肯定会有这种需求),有两种方法可供参考。第一种就是通过for循环,对集合进行遍历,注意这里是通过集合遍历,而不是容器;另一种就是如果就是想要通过容器进行遍历的话,就必须再为更新后的集合创建一个新的容器,然后通过这个新的容器进行遍历。