for循环遍历集合时add或remove

之前使用foreach循环add数组元素时抛出ConcurrentModificationException,故总结如下:
在这里插入图片描述
运行:foreach循环时调用add,抛出java.util.ConcurrentModificationException异常:
在这里插入图片描述
可参考阿里编程手册的foreach中remove/add反例,截图如下:
在这里插入图片描述
上述例子把’1"换成"2",remove时则不会抛出异常。故分析如下:

示例代码:

List<String> list = new ArrayList<>();
list.add("e1");
list.add("e2");

for (String str : list) {
    if ("e1".equals(str)) {
        list.remove("e1");
    }

    if ("e2".equals(str)) {
        System.out.println("element 2 fetched");
    }
}

运行结果:element 2 fetched 将不会被打印。
为什么remove(e1)会导致无法获取e2?
来看看将.class文件反编译后得到的代码,实际上编译器将 foreach 转换成了用 Iterator 来处理:

ArrayList list = new ArrayList();
list.add("e1");
list.add("e2");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    if("e1".equals(str)) {
        list.remove("e1");
    }

    if("e2".equals(str)) {
        System.out.println("element 2 fetched");
    }
}

当 list.remove(“e1”)后,在 while(var2.hasNext()) 时,返回结果将为 false,因此当循环一次后Iterator将认为list已经遍历结束。

要弄清原因,需要看看ArrayList对于Iterator接口的实现,了解hasNext()、next()方法的实现。

1、先看看ArrayList中实现Iterator的内部类It:

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    ...
}  

cursor表示下一个返回元素的下标,可以理解成 游标;lastRet表示上一次返回的元素下标;另ArrayList有个size属性,表示ArrayList中的元素个数。
2、再看hasNext()方法:

public boolean hasNext() {
    return cursor != size;
}

hasNext() 的判断条件是cursor != size. 只要没遍历到最后一个元素,就返回true。
3、下面是 next() 部分代码:
public E next() {

int i = cursor; // cursor为当前需要返回元素的下标

cursor = i + 1; // cursor向后移动一个位置,指向下一个要返回的元素
return (E) elementData[lastRet = i]; // 对lastRet赋值,然后返回当前元素
}
现在,看一下下面代码的运行情况:

ArrayList list = new ArrayList();
list.add("e1");
list.add("e2");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
    String str = (String)var2.next();
    if("e1".equals(str)) {
        list.remove("e1");
    }
}

第一次 调用var2.hasNext(),此时满足条件 cursor(0) != size(2),然后执行 var2.next(),此时 cursor=1
执行 list.remove(“e1”),此时,list的size将从2变为1
当执行完第一次循环,进入第二次hasNext()判断时,cursor=1而且size=1,导致Iterator认为已经遍历结束,因此e2将被漏掉。
list本有2个元素,Iterator第一次获取元素时,程序删掉了当前元素,导致list的size变为1。Iterator第二次获取元素时,发现list一共只有一个元素,被认为已经遍历完成!
所以,hasNext() 是根据已fetch元素和被遍历对象的size动态判断的,一旦遍历过程中被遍历对象的size变化,就会出现以上问题。

用普通for循环进行处理:
在这里插入图片描述
原因:局部变量length为list遍历前的size,length=2;remove(“e1”)后,list的size变为1;因此,第二次进入循环执行list.get(1)时将出现上述异常

正确的姿势:
将remove操作交给Iterator来处理,使用Iterator接口提供的remove操作。

List<String> list = new ArrayList<>();
list.add("e1");
list.add("e2");

for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
    String str = iterator.next();
    if ("e1".equals(str)) {
        iterator.remove();
    }

    if ("e2".equals(str)) {
        System.out.println("element 2 fetched");
    }
}

运行结果:element 2 fetched 被正常打印出来。

那Iterator的remove()又是怎么做的?下面是ArrayList中迭代器的remove方法。

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet); // 调用ArrayList的remove移除元素,且size减1
        cursor = lastRet; // 将游标回退一位
        lastRet = -1; // 重置lastRet
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

因为Iterator.remove()在执行集合本身的remove后,同时对游标进行了 “校准”。

关于ConcurrentModificationException
在Java集合框架中,很多对象都不是线程安全的,例如:HashMap、ArrayList等。当Iterator在遍历集合时,如果其他线程操作了集合中的元素,将抛出该异常:

private static List<String> list = new ArrayList<>();
private static boolean isListUpdated = false;

public static void main(String[] args) throws InterruptedException {
    list.add("e1");
    list.add("e2");

    new Thread(() -> {
        list.add("e3");
        isListUpdated = true;
    }).start();

    for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
        while (!isListUpdated) {
            Thread.sleep(1000);
        }
        iterator.next();
    }
}

ArrayList中对于Iterator的实现类为Itr如下:

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;
}    

其中有个重要的属性 expectedModCount,表示本次期望修改的次数,初始值为modCount,
modCount 是 AbstractList 的属性,如下:

protected transient int modCount = 0;

注意,它由transient修饰,保证了线程之间修改的可见性。对集合中对象的增加、删除操作都会对modCount加1。
在next()、remove()操作中都会进行 checkForComodification() ,用于检查迭代期间其他线程是否修改了被迭代对象。下面是checkForComodification方法:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

这是一种 Fail-Fast(快速失败) 策略,只要被迭代对象发生变更,将满足 modCount != expectedModCount> 条件,从而抛出ConcurrentModificationException。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值