ArrayList 不能用 foreach 增删改元素

一、快速失败(fail-fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException

1️⃣原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

2️⃣注意:这里异常的抛出条件是检测到modCount!=expectedmodCount这个条件。如果集合发生变化时,修改 modCount 值刚好又设置为 expectedmodCount 值,则不会抛出异常。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。

3️⃣场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二、安全失败(fail-safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

1️⃣原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。

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

3️⃣场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

三、引出问题

1️⃣listA 的 remove 方法成功执行。

List<String> listA = new ArrayList<String>();
   listA.add("1");
   listA.add("2");
for (String s : listA) {
   if("1".equals(s)){
	  listA.remove(s);
   }
}

2️⃣listB 的 remove 方法运行抛出 ConcurrentModificationException。

List<String> listB = new ArrayList<String>();
	listB.add("2");
	listB.add("1");
for (String s : listB) {
	if("1".equals(s)){
	  listB.remove(s);
	}
}

四、为了寻找原因,查看源码

增强 for 循环,其实是依赖了 while 循环和 Iterator 实现的。所有的 Collection 集合类都会实现 Iterable 接口。找到 ArrayList 类的 Iterator():使用自己的 Itr 内部类,并且实现了 Iterator 接口。

迭代器的本质是先调用 hasNext() 判断是否存在下一个元素,然后再使用 next() 取下一个元素:Itr 内部类实现

/**
 * An optimized version of AbstractList.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;

    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();
    }
}

1️⃣listA 能 remove 成功,是因为它只循环了一次。因为它在 remove 元素 1 之后,它的 size - 1 变成 1,然后 Itr 内部的 cursor 变量由 0 变成 1。此时 1 = 1,循环结束,所以成功了。
2️⃣listB 却 remove 失败,其实它在循环第二次的时候,也 remove 成功了,但是第三次判断 next 的时候 cursor 的值为 2 导致不等于现在的 size 1,所以执行了 next 方法。最重要的来了,之前 remove 的操作导致 ArrayList 的 modCount 值加 1,然后 Itr 类中的 expectedModCount 保持不变,所以会抛出异常。

同理可得,由于 add 操作也会导致 modCount 自增,所以不允许在 foreach 中增删改 ArrayList 中的元素。对此,推荐使用迭代器 Iterator 删除元素:

Iterator<String> iterator = arrayList2.iterator();
while(iterator.hasNext()){
	String item = iterator.next();
	if("1".equals(item)){
		iterator.remove();
	}
}

如果存在并发操作,还需要对 Iterator 进行加锁操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值