为什么foreach中不能进行arrayList元素的remove/add操作?

前言

小编之前剖析了 HashMap 和 ConcurrentHashMap 底层原理,需要的小伙伴可以点击传送门跳转。

深度剖析HashMap一篇文章就够了

一文深度剖析ConcurrentHashMap

步入正题

我们先来验证一下,foreach 中是不是真的不能对 arraylist 进行 remove/add 操作?

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
for (String str : list) {
    list.remove(str);
}

执行以上程序后会有以下结果:

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 com.dmsd.test.virtual.TestVirtual.main(TestVirtual.java:32)

根据报错信息,我们可以得出以下信息:

  • 报错位置在 main 方法中,异常类型为ConcurrentModificationException
  • 内部报错位置在 ArrayList$Itr.checkForComodification ,是 ArrayList 的内部类 Itr 中的 checkForComodification 方法

得知以上信息了,我们看看程序代码经过反编译之后是什么样的?

List<String> list = new ArrayList();
list.add("3");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    list.remove(str);
}

以上为程序反编译后的结果,我们可以看到抛开 foreach 语法糖外衣内部的实现方式,使用 ArrayList 内部实现的 Iterator 去遍历,每次遍历前会判断是否存在下一个元素,如存在获取下一个元素进行 remove

看起来没什么毛病,为什么报异常呢?我们来到 ArrayList 内部的 Iterator 实现过程去找找原因。

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;
    ......
}

以上是 ArrayList 的内部类 Itr ,我们使用 foreach remove/add arraylist 元素时,使用的就是这个内部类。我们根据反编译后的代码来看看每个方法都是如何执行的?

var2.hasNext() 方法

// Itr 内部类的 hasNext()
public boolean hasNext() {
  return cursor != size;
}

其中,size 是 ArrayList 类的成员变量,用来记录 arraylist 中的元素个数。cursor 是 ArryayList 内部类 Itr 的成员变量,是个遍历元素的指针。每次执行 hasNext() 都会判断当前指针是否指向了最后一个元素,如否,则代码有下个元素,返回 true

var2.next() 方法

// Itr 内部类的 next()
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];
}

我们再来看看 next() 是如何实现的?

我们看到 next() 内部第一行就调用了 checkForComodification() ,点进去看看是check 什么的。

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

哦~ 这不就是我们刚才报的异常嘛! modCount 是 ArrayList 中的成员变量,用来记录list被修改结构的次数(add/remove 操作记为一次结构修改,而根据索引赋值不属于结构的修改);expectedModCount 是 ArrayList 内部类 Itr 的成员变量,上面我们可以看到在声明时候就用 modCount 给 expectedModCount 赋值了。而 next() 方法中获取 下一个元素值时,会首先判断 这两个字段是否相等,如不相等,则认为此list 的结构被修改过,这样拿到的下一个元素的值就不是准确的值,所以会直接抛出异常。

在foreach中当 remove/add 第一个元素后,其实是可以操作成功的,因为初始化内部类 Itr 时,会完成一次 expectedModCount = modCount; 赋值操作,所以此时 两个变量相等,不会抛出异常,会 操作成功。(经过打断点调试已验证)由于在 remove 过程中,会给 modCount++(add操作也一样会给 modCount++),记录此list结构的修改的次数增加了一次。所以,在 foreach 中当操作第二个元素时,并没有将 ArrayList 的成员变量 modCount 重新赋值给 ArrayList 内部类 Itr 的 expectedModCount 成员变量,导致两个变量值不等,以致于抛出 ConcurrentModificationException

// ArrayList 的 remove()
public E remove(int index) {
   rangeCheck(index);

   modCount++;
   E oldValue = elementData(index);

   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

   return oldValue;
}

明白了为什么不能在 foreach 中对 arrayList 进行 remove/add 操作了,那如果我们真的需要遍历对list进行操作怎么办?循环 remove/add 操作 arrayList 的正确姿势:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.remove();
}

我们分析一下,这两种写法本质上有什么区别?

调用 remove() 的对象不一样,一个是调用的 ArrayList 这个外部类的 remove 方法,一个调用的 ArrayList 内部类 Itr 的 remove() ,我们看看 ArrayList 内部类 Itr 的 remove() 是如何实现的?

// ArrayList 内部类 Itr 的 remove()
public void remove() {
   if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
   		// 调用外部类 ArrayList 的 remove() 方法
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        // 将结构修改的次数重新复制给内部类变量expectedModCount 
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

我们可以看到,在调用外部类 ArrayList 的 remove() 方法做了 remove 操作后,有一个给 expectedModCount 变量粉赋值的操作,也就是在 外部类的 remove() 中会给 modCount++ ,导致报异常的原因就是因为 expectedModCount 和 modCount 值不等,那索性每次 remove() 完后重新给 expectedModCount 赋值就能保证每次操作 list 后 两个变量值相等了,也就不会报异常了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值