快,关注这个号,一起涨姿势~
foreach循环中为什么不要进行remove/add操作
从阿里的代码规范中我们可清楚看到foreach循环中不要remove/add操作字眼。具体见下图:
接下来,我们不妨试一试,看看究竟吧,大家跟我来!!!
一.从阿里规范中看代码
我们把阿里的代码摘下来敲一敲:
List list = new ArrayList();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
我们在idea执行一下,没有问题,但是需要注意,此时循环只执行了一次。再来看一段会出问题的代码:
List list = new ArrayList();
list.add("1");
list.add("2");
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
控制台输出为:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at Foreach.main(Foreach.java:16)
异常出现了,原因是什么昵?我们接着分析!!!
二.怎么回事昵?
从上看是不是很奇怪?真是纳闷呀!!!,接下来对class文件,反编译下(idea支持反编译,点一下.class文件即可查看),代码如下:
List list = new ArrayList();
list.add("1");
list.add("2");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String item = (String)var2.next();
if ("2".equals(item)) {
list.remove(item);
}
}
可以发现,原本的增强for循环,其实是依赖了while循环和Iterator实现的。原因还没出,我们慢慢往下看。
在往下看代码之前我们先牢记几个词:
- modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
- expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。
- Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器就是Itr类的实例。
(1)我们进去list.remove()方法及查看核心方法fastRemove()。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
由上可以看到list做remove操作,modCount是改变的,也就是操作一次累加1。
(2)顺路观察下list.add()方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这个方法modCount也会累加。
(3)再去观察下,iterator()方法、Iterator的实现、checkForComodification()方法
public Iterator iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
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();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
简单总结一下,ArrayList是线程不安全的,在被修改后再继续迭代就报错。由Iterator实现可以看到,当迭代时,会将modCount暂存在expectedModCount中,
在获取下一个元素时,都检查修改次数是否有变动,有变动则不再继续迭代,而是抛出错误ConcurrentModificationException。
好了,大体原因就是这样了。然而这个问题要如何避免昵?我们看一下第三个部分即可。
三、如何避免
(1)直接使用普通for循环进行操作,代码如下:
List list = new ArrayList();
list.add("1");
list.add("2");
for (int i = 0; i < list.size(); i++) {
if ("2".equals(list.get(i))) {
list.remove(list.get(i));
}
}
(2)直接使用Iterator进行操作,阿里推荐,代码如下:
List list = new ArrayList();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals("2")) {
iterator.remove();
}
}
(3)使用Java 8中提供的filter过滤
List list = new ArrayList();
list.add("1");
list.add("2");
ist = list.stream().filter(l -> !l.equals("2")).collect(Collectors.toList());
(4)直接使用fail-safe的集合类
fail-fast,即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。
同时需要注意的是,即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。
代码如下:
ConcurrentLinkedDeque list = new ConcurrentLinkedDeque() {{
add("1");
add("2");
}};
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
(5)使用增强for循环其实也可以
如果,我们非常确定在一个集合中,某个即将删除的元素只包含一个的话, 比如对Set进行操作,那么其实也是可以使用增强for循环的,只要在删除之后,立刻结束循环体,不要再继续进行遍历就可以了,也就是说不让代码执行到下一次的next方法。
代码如下:
List list = new ArrayList<>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
上面这个例子较特殊,也就是第二次循环它进不去了,非特殊例子,大家可用break结束。
四、针对上述内容做个小结
我们使用的增强for循环,其实是Java提供的语法糖,其实现原理是借助Iterator进行元素的遍历。
但是如果在遍历过程中,不通过Iterator,而是通过增强for循环,集合类自身的方法对集合进行添加/删除操作。那么在Iterator进行下一次的遍历时,经检测发现有一次集合的修改操作并未通过自身进行,那么可能是发生了并发被其他线程执行的,这时候就会抛出异常,来提示用户可能发生了并发修改,这就是所谓的fail-fast机制。
当然还是有很多种方法可以解决这类问题的。比如使用普通for循环、使用Iterator进行元素删除、使用Stream的filter、使用fail-safe的类、使用增强for
循环也可以但要注意事项等。