foreach中remove/add报错问题?!

  • Post author:
  • Post category:其他




– 问题引入

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);
	}



– 总结

  1. foreach是java提供的语法糖, 内部实现是借助iterator进行的元素遍历.
  2. foreach遍历过程中, 如果不使用iterator的remove等方法, 而使用集合类自身的remove等方法. 那么在iterator调用next方法时,会抛出异常, 来提示用户可能发生了并发修改, 这就是fail-fast机制.



版权声明:本文为weixin_43872459原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。