在阿里巴巴java开发手册中有这样一条:
【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
System.out.println(list);
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
System.out.println(list);
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的 结果吗?
反例执行代码结果:
反编译后代码:
List list = new ArrayList();
list.add("1");
list.add("2");
System.out.println(list);
for(Iterator iterator = list.iterator(); iterator.hasNext();)
{
String item = (String)iterator.next();
if("1".equals(item))
list.remove(item);
}
System.out.println(list);
我们知道foreach遍历的本质其实就是使用迭代器来遍历,编译器在编译时给我们自动进行了代码的改写
问:不是说不能在foreach中进行元素的增加删除操作么,为什么会成功呢?
答:不是不能在foreach中进行元素的增加删除操作,在一些条件下是可以在foreach中进行元素的增加删除操作的,但是不推荐在foreach中进行元素的增删操作.
把“1”换成“2”后,执行结果:
首先我们先看一下为什么会抛出这个异常(并行修改异常)
反编译后代码:
public class LianXi
{
public LianXi()
{
}
public static void main(String args[])
{
List list = new ArrayList();
list.add("1");
list.add("2");
System.out.println(list);
for(Iterator iterator = list.iterator(); iterator.hasNext();)
{
String item = (String)iterator.next();
if("2".equals(item))
list.remove(item);
}
System.out.println(list);
}
}
我们可以看到,foreach循环遍历元素时使用的是迭代器的方法进行遍历,而调用list本身的方法进行删除操作,通过打断点发现,循环中通过list的删除方法进行删除元素后,并没有在此时抛出异常,而是在调用迭代器的next方法时抛出此异常,迭代器的next()方法中会调用checkForComodification()方法进行判断,在此方法中抛出了并行修改异常,因为迭代器进行遍历时使用的是集合的一个拷贝,而并非对list集合进行直接访问,迭代器在使用next()方法时会检查list元素是否发生了变化,当在循环中使用了list集合本身的删除方法时,list集合中的元素产生了变化,在下一次使用迭代器的next()方法时,此方法就会发现集合发生了改变,从而抛出并行修改异常.
这里我们来看看Java里AbstractList实现Iterator的源代码:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { // List接口实现了Collection<E>, Iterable<E>
protected AbstractList() {
}
...
public Iterator<E> iterator() {
return new Itr(); // 这里返回一个迭代器
}
private class Itr implements Iterator<E> { // 内部类Itr实现迭代器
int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() { // 实现hasNext方法
return cursor != size();
}
public E next() { // 实现next方法
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() { // 实现remove方法
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
总之:因为迭代器是一个访问者,list集合本身也是一个访问者,存在两个访问者,当使用list修改了自身的元素,迭代器再次访问时就会发现集合已经发生了变化,进而抛出并发修改异常
我们在list.remove()方法后加入break语句后,程序便不会抛出异常,因为在list元素改变后,迭代器没有调用next()方法进行检查.
同理:
现在我们已经知道了在list集合在进行了增删操作后,再次使用迭代器的next()方法会抛出异常,那为什么在第一个例子中删除第一个元素后却没有抛出异常呢?
这里我现在也没搞清楚,应该是与迭代器底层实现原理有关,待我弄清楚后补上
最后我们来回答一下为什么不要在 foreach 循环里进行元素的 remove/add 操作
因为迭代器是一个访问者,list集合本身也是一个访问者,存在两个访问者,当使用list修改了自身的元素,迭代器再次访问时就会发现集合已经发生了变化,进而抛出并发修改异常.而即使通过循环中加入if条件和break语句,进而使用集合本身的增删操作时,只能执行if中的代码一次,失去了遍历本身的意义,所以不推荐在 foreach 循环里进行元素的 remove/add 操作,而使用迭代器本身的remove/add 操作