foreach 循环里进行元素的 remove/add 操作

不要在 foreach 循环里进行元素的 remove/add 操作

1、remove 元素请使用 Iterator方式,如果并发操作,需要对Iterator对象加锁

// A code block
List<String> a = new ArrayList<String>();
a.add("1");
a.add("2");
for (String temp : a) {
if ("1".equals(temp)) {
a.remove(temp);
}
}

说明:如果将if (“1”.equals(temp)) { 里面的 1 换成 2的话,会报下面这个错误

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.song.pojo.Test01.test01(Test01.java:28)
	at com.song.pojo.Test01.main(Test01.java:17)

**原因:**所以,之所以会抛出ConcurrentModificationException异常,是因为代码中使用了foreach循环,而在foreach循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用集合类自己的方法,这就导致Iterator在遍历的时候会发现有一个元素在自己不知不觉的情况下被添加/删除了,就会抛出一个异常,用来提示用户可能发生了并发修改。

2、解决办法?

2-1、直接使用普通for循环 遍历
因为普通for循环并没有使用Iterator的遍历,所以压根就没有进行fail-fast的检验
2-2、直接使用Iterator 遍历
如何直接使用iterator提供的add/remove方法,那么就可以修改到expectedModCount的值,那就不会再抛出这个异常了,其实现的代码如下:

public void remove() {
	if (lastRet < 0)
		throw new IllegalStateException();
	checkForComodification();
	try {
		ArrayList.this.remove(lastRet);
		cursor = lastRet;
 		lastRet = -1;
		expectedModCount = modCount;
	} catch (IndexOutOfBoundsException ex) {
		throw new ConcurrentModificationException();
	}
 }

2-3、使用Java 8中提供的filter过滤:
Java 8中可以把集合转换成流,对于流有一种filter操作,可以对原始Stream进行某项测试,通过测试的元素被留下来生成一个新的流:

public static void main(String[] args) {
		List<String> userNames = new ArrayList<String>();
		userNames.add("Tom");
		userNames.add("Jack");
		userNames.add("Mic");
		userNames.add("Seven");
		userNames = userNames.stream().filter(userName -> !userName.equalsIgnoreCase("Jack")).collect(Collectors.toList());
		System.out.println(userNames);
	}

2-4、直接使用fail-safe的集合类:
在Java中,除了一些普通的集合类以外,还有一些采用了fail-safe机制的集合类,这样的集合容器在遍历时不时直接在集合内容*问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所做的修改并不能被迭代器检测到,所以不会抛出ConcurrentModificationException。

public static void main(String[] args) {
		ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>();
		userNames.add("Tom");
		userNames.add("Jack");
		userNames.add("Mic");
		userNames.add("Seven");
		for (String userName : userNames) {
			if (userName.equals("Jack")) {
				userNames.remove(userName);
			}
		}
		System.out.println(userNames);
	}

基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

以上几种方式都可以避免触发fail-fast机制,避免抛出异常。如果是并发场景,建议使用concurrent包中的容器;如果是单线程场景,Java8之前的代码中建议使用Iterator进行元素删除/添加;Java8之后可以考虑使用Stream及filter.

上一篇:11月编程


下一篇:机器学习 朴素贝叶斯分类垃圾邮件