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

在阿里巴巴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 操作

 

 

 

 

 

 

 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值