我们在敲代码的过程中,难免会和一些Exception不期而遇,就像不知什么时候就会触碰到的女朋友的小脾气,并且,Exception也正像女朋友一样,你需要花上一些时间去了解对方,才知道自己是怎么“死”的。和ConcurrentModificationException的初相遇,那是在一个秋末夜晚,昏黄的路灯下……好了,不扯了。
现在,我们来会一会而ConcurrentModificationException。问题出现在做一个模拟影院自主购票的控制台小项目的过程中,需要一个方法来将选中的电影加入到购物车列表。具体如下。
Iterator<MovieInCart> it = chosenList.iterator();
if(it.hasNext()==false){ //购物车列表为空
chosenList.add(movieInCart);<span style="white-space:pre"> </span> //直接将该电影加入购物车列表
}else{
while(it.hasNext()){ //判断购物车列表里是否已经有该电影
<strong>MovieInCart movieExisted = it.next();//该语句报错 ConcurrentModificationException</strong>
if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
} else{
chosenList.add(movieInCart);//没有的话 将该电影加入购物车列表
}
}
}
解决
解决方法,直接在chosenList.add(movieInCart);这一语句后加一个break跳出while循环即可。
while(it.hasNext()){ //判断购物车列表里是否已经有该电影
MovieInCart movieExisted = it.next();
if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
} else{
chosenList.add(movieInCart);//没有的话 将该电影加入购物车列表
<strong>break;//程序若能执行到此,已将选择的电影添加,无需在进行下一次循环</strong>
}
}
另外用增强的for循环的话,也一样加break;即解决问题,只是代码与用迭代器稍有不同,本文在后面也会讲到。
for(MovieInCart movieExisted:chosenList){
if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
break; //有一个满足条件修改后的即退出循环</strong>
} else{
chosenList.add(movieInCart);//没有的话 将该电影加入购物车列表
break; //添加后即退出循环
}
}
进一步
ConcurrentModificationException意为并发的修改异常,也就是说ConcurrentModificationException容易在我们对ArrayList对象进行多种操作时出现,尤其是循环中。至此,我们所遇到的问题算是已经解决,读者如果只是想要不再看到这恼人的异常,可以就此结束本文的阅读,仔细看看自己的代码有没有用多次的不同的操作修改AraayList对象。
接下来,让我们深入源代码腹地,看看到底是发生了什么。
报错内容很明确地提供了信息:
从上面的报错信息我们可以看出,异常的始发地点是checkForComodification()方法,我们点击后面的提示找到该方法:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
该方法在ArrayList类下的Itr类中,而Itr类是Iterator接口的实现类,也就是说,当我们对ArrayList用到迭代器时,调用迭代器的方法实际上是调用了Itr类的方法。从checkForComodification()方法中,我们看出异常出现的原因是 modCount != expectedModCount 。这俩又是什么呢?
讲到这,我们应该着手了解一下ArrayList类下的Itr类的一些属性了。
int cursor; // index of next element to return 下一元素的下标
int lastRet = -1; // index of last element returned; -1 if no such 最后一个元素的下标,没有元素则为-1
int lastRet = -1; // index of last element returned; -1 if no such 最后一个元素的下标,没有元素则为-1
int expectedModCount; //初始为modCount,作用是使迭代器的修改次数与ArrayList的相同。
int modCount; //实际上这是AbstractList中的属性。The number of times this list has been structurally modified.
//该列表被结构性的修改过的次数。structurally ,我们可以理解为任何改变其属性的操作都是structurally的,如next(), remove(), add(E e)等。
乍看上去,似乎记录修改次数并没有什么作用。实际上,modCount和expectedModCount的作用,就是用来判断是否出现了并发修改异常。因为用迭代器遍历的时候,在单线程中,代码开发者是不允许同时对数组列表进行增删等操作的。
如果没有modCount和expectedModCount,而我们又像本文最开始的程序那样调用了ArrayList的add()方法会怎样呢?
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
<strong>cursor = i + 1;</strong>
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
查看add(E e)方法的代码我们可以知道,那会对迭代器的游标cusor进行+1操作,而同时add(E e)方法本质还是调用了add(int index, E e)两个参数的方法:
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
为避免上述这样的操作混乱,所以add()、remove()、next()等众多方法中才有了checkForComodification(),因此,有了ConcurrentModificationException。
至此,犹抱琵琶欲露还羞的ConcurrentModificationException终于完全现出芳容。只要我们注意不要在用迭代器的同时三番两次地“得罪”ArrayList对象,她就不会随便发脾气。
不过即使这样,我们还是可以在修改ArrayList上有较大的自由度,只要我们想,并且,只要我们够巧妙。
还有什么
笔者想,既然我们用迭代器会出现这种情况,那么改用增强的for循环呢?
将while循环修改,我们得到如下的代码:
for(MovieInCart movieExisted:chosenList){
<span style="white-space:pre"> </span>if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
} else{
chosenList.add(movieInCart);//没有的话 将该电影加入购物车列表
}
}
然而,运行程序再次调试,却出现了和用迭代器一模一样的出此同一位置的异常:
说明,增强的for循环的本质,还是迭代器,只不过是隐含的。所以我们还是要在add()方法后加上break; 。
另外,在再次调试中,笔者还发现,添加重复的电影时出现了问题,蝙蝠侠大战超人的数量,笔者先是输入了12,后输入了1,却出现了下图的情况。
排查半天,笔者发现,是因为if语句中没有break;,所以程序又进入了下一次循环又执行了一次else语句。所以最终正确的代码应该是:
for(MovieInCart movieExisted:chosenList){
if(movieInCart.equals(movieExisted)){//有的话 仅更改已存在的该电影的购买数量
movieExisted.setQuantity(movieExisted.getQuantity()+quantity);
break; //有一个满足条件修改后的即退出循环</strong>
} else{
chosenList.add(movieInCart);//没有的话 将该电影加入购物车列表
break; //添加后即退出循环
}
}
最后,不错,我们与 ConcurrentModificationException的会面还算愉快。
不过,尽管我们最终得到了满意的代码,但是在写程序时,我们还是要尽量避免在遍历动态数组的同时对其进行过多的修改操作。