- 问题引入
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("james");
list.add("curry");
for (String s : list) {
if (s.equals("kobe")){
// list.remove(s);
list.add("kyrie");
}
}
System.out.println(list);
}
如上代码, 当在foreach循环中remove或者add时抛出异常:
根据报错信息, 得到调查的目标就在ServletDemo.main(), ArrayList$Itr.next(), ArrayList$Itr.checkForComodification()方法中, 依次查看.
- ServletDemo.main()
基本没有什么头绪, 跟到下一层.
- ArrayList$Itr.next()
发现问题在checkForComodification()方法中, 继续跟进.
- ArrayList$Itr.checkForComodification()
找到了抛出异常的位置. 当modCount != expectedModCount时, 抛出异常. 调查有了新的目标: modCount和expectedModCount分别是什么? 有什么作用?
- modCount和expectedModCount
- modCount
modCount是由ArrayList的父类AbstractList继承而来的, 由注释和查阅的资料发现: 在ArrayList中, 所有设计结构变化的方法都会增加modCount的值, 例如[ add(), remove(), addAll(), removeRange(), clear() ]方法.这些方法每调用依次, modCount的值就会加一. 总结来说, modCount的功能类似于在记录对此集合的修改次数.
- expectedModCount
在ArrayList中发现了expectedModCount, 通过调用iterator()方法使用了一个私有内部类, 生成了一个Itr对象返回. Itr实现了Iterator()接口, 其中定义了一个int型的expectedModCount, 这个属性在Itr类初始化时被赋值为ArrayList对象的modCount值.
总结:(个人粗略总结)
class ArrayList{
private int modCount; //表示该集合实际被修改的次数
public void add(); //添加方法
public void remove(); //删除方法
private class Itr implements Iterator<E>{
int expectedModCount = modCount; //当Itr被初始化时将modCount的值赋给expectedModCount
//expectedModCount目前还没有了解它的作用, 根据英文翻译大概是预期的某种数量的计量.
}
public Iterator<E> iterator(){
return new Itr();
}
}
当Itr类被初始化时expectedModCount是等于modCount的, 而异常的抛出条件是当两个变量值不等时, 调查的目标转变为两个变量是合适不等的?
- modCount和expectedModCount使用过程
当我在coreach中remove或者add时, 抛出了异常, 所以我到remove和add中查找导致两个变量不等的信息:
在ArrayList的源码中, 以remove为例发现了问题, 在执行remove、add、clear等操作时, 只修改了modCount, 而没有修改expectedModCount, 从而导致了异常的抛出.
- 深度探究
完整的还原了异常抛出的经过, 但是还没有结束: 通过查阅资料, 发现foreach是一种语法糖的写法. [ 语法糖 : 也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖. ]
将增强for解糖之后得到的代码应该是这样的:
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("kobe");
list.add("james");
list.add("curry");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String item = (String)var2.next();
if (item.equals("kobe")){
//list.remove(s);
list.add("kyrie");
}
}
}
通过解糖之后的代码, 发现一个问题: 我们通过Iterator进行遍历, 而Iterator接口中是有需要实现的remove()方法的, 而我却使用ArrayList类中的remove方法去删除. 于是发现了新的线索: ArrayList中的Itr内部私有类继承了Iterator接口, 一定实现了remove方法.
- Itr中的remove方法
发现: 它先调用了ArrayList中的remove方法, 之后又将modCount的值赋给expectedModCount, 保证了两者的值相等. 也就是说, 使用Itr中重写的remove方法, 两者的值就不会不等, 就不会抛出异常了. 以下为实验代码:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("james");
list.add("curry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String item = iterator.next();
if (item.equals("kobe")){
iterator.remove();
}
}
System.out.println(list);
}
果然, 运行成功, 没有报错. 最终理解之所以会抛出异常, 是因为遍历是通过iterator进行的, 但元素的增删是通过集合类自己的方法, 导致iterator在遍历的时候, 在自己不知情的情况下多了或者少了元素, 从而抛出了异常.
- fail-fast总结
有两个线程A、B, 其中线程A负责遍历list, 而线程B负责修改list. 线程A在遍历list过程中(expectedModCount = modCount = N), 线程B启动, 并增加一个元素, 此时modCount的值发生改变 (modCount + 1 = N + 1). 线程A继续遍历执行next方法, 通过checkForComodification方法发现expectedModCount = N, 而modCount = N + 1, 两者不等, 抛出ConcurrentModificationException异常, 从而产生fail-fast机制.
当遇到fail-fast机制时, 应该怎么处理?
1. 使用普通for循环操作
普通for循环没有用到Iterator遍历, 因此没有进行fail-fast检验.
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("james");
list.add("curry");
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("kobe")){
list.remove(i);
}
}
System.out.println(list);
}
2. 使用Iterator操作
使用Iterator中的remove方法
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("james");
list.add("curry");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String item = iterator.next();
if (item.equals("kobe")){
iterator.remove();
}
}
System.out.println(list);
}
3. 使用filter过滤操作
在jdk1.8中可以把集合转化成流, 对于流有一种filter操作, 可以对原始Stream进行某项测试, 通过测试的元素被留下来生成一个新的Stream.
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("james");
list.add("curry");
list = list.stream().filter(l -> !l.equals("kobe")).collect(Collectors.toList());
System.out.println(list);
}
4. 依旧可以使用foreach(特定条件下)
根据上文得知, 异常抛出是因为, modCount和expectedModCount两值判断不等所抛出的异常, 是从next方法中进入的判断方法. 所以当我们非常确定在一个集合中, 某个即将删除的元素只包含一个的话, 或者对Set进行操作, 那么依旧可以使用foreach, 只要在删除后立即结束循环, 不再继续遍历就不会让代码执行到next方法.
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("james");
list.add("curry");
for (String s : list) {
if (s.equals("kobe")){
list.remove(s);
break;
}
}
System.out.println(list);
}
5. 使用fail-safe的集合类
除了一些普通的集合类以外, 还有一些采用了fail-safe机制的集合类.这样的集合容器在遍历时不是直接在集合内容上访问的, 而是先赋值原有集合内容, 在拷贝的集合上进行遍历. 由于迭代时是对原集合的拷贝进行遍历, 所以对原集合的修改迭代器不会检测到.
public static void main(String[] args) {
ConcurrentLinkedDeque<String> list = new ConcurrentLinkedDeque<>();
list.add("kobe");
list.add("james");
list.add("curry");
for (String s : list) {
if (s.equals("kobe")){
list.remove(s);
break;
}
}
System.out.println(list);
}
- 总结
- foreach是java提供的语法糖, 内部实现是借助iterator进行的元素遍历.
- foreach遍历过程中, 如果不使用iterator的remove等方法, 而使用集合类自身的remove等方法. 那么在iterator调用next方法时,会抛出异常, 来提示用户可能发生了并发修改, 这就是fail-fast机制.