一种较为隐蔽ConcurrentModificationException情形
ConcurrentModificationException是遍历过程中修改list而抛出的错误。就像前面分析的,单线程时这种错误主要是因为使用forEach造成:遍历一个拷贝,修改原始list,造成的。简单的大家都知道,这里要说一种比较隐蔽的出错方式:
1 public class ExceptionTest{ 2 3 private List<String> list = new ArrayList<>(); 4 5 public ExceptionTest(){ 6 for(int i=0;i<9;i++){ 7 list.add("string"+i); 8 } 9 } 10 11 public void throwExceptions(){ 12 List<List<String>> listList = new ArrayList<>(); 13 listList.add(list.subList(0,3)); 14 listList.add(list.subList(3,6)); 15 listList.add(list.subList(6,8)); 16 Iterator<List<String>> it1 = listList.iterator(); 17 while(it1.hasNext()){ 18 List<String> l = it1.next(); 19 Iterator<String> it = l.iterator(); 20 while(it.hasNext()){ 21 String str = it.next(); 22 if(str.endsWith(("5"))){ 23 it.remove(); 24 } 25 } 26 } 27 } 28 }
运行上面的代码还是会出错,虽然两层循环都用来Iterator。如果不仔细看很难发现这其中的原因。当你调试你会发现,删除以5结尾的字符串的时候,listList中的第一个子list会报错,而不是正在做修改的第二个子list。所以这个错误的来源不是两个遍历的本事,而是listList的初始化方式,可以看到13到16行,每个子列表使用的是list的subList方法。进一步查看subList代码可以发现。这个方法其实并未新建list而是返回了原理list的一个视图(其实索引):
SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; }
所以在修改第二个子list的时候回引起父列表(list)状态发生改变,虽然两层遍历看起来是正常的,但是他们却是在遍历和修改同一个list,因此会抛出ConcurrentModificationException异常。
总结:有时候的异常并不是因为当前的步骤引起的,原因有可能来自于之前的某一步骤。