foreach循环ArrayList时不能使用ArrayList的增删方法之源码简析

首先,虽然这篇博文的名称是foreach循环ArrayList时不能使用ArrayList的增删方法之源码简析,但是源码简析的主要部分其实在另一篇博文(foreach迭代ArrayList时,真的不能删除元素吗?,以下简称原博文)。

本文其实是基于原博文做了点小小的扩展。所以在继续往下阅读之前,请先阅读原博文,否则可能会不太能理解我下面在说些什么!

(为什么不再自己从头分析一遍相关源码呢?一是因为我比较懒,但是最主要还是因为我觉得原博文已经分析得很好很清晰了。)

——————————————————— 手动分割线 ————————————————————

既然看到了这里,那么我就默认你已经阅读过原博文了,接下来就先解答原博文中作者在文末提出的问题。

为了方便阅读,这里再把问题贴出来:
在这里插入图片描述
其实吧,我觉得在作者提的问题中已经给出了答案:执行完这次打印,进入下一次迭代时,又产生了checkForComodification异常。

为什么又产生了checkForComodification异常?就是因为又进入了下一次迭代,又先后执行了hasNext()方法和next()方法,在next()方法中做checkForComodification()校验时,因为modCount != expectedModCount,所以抛出异常。

那么,为什么循环到倒数第二个元素时,remove一个元素不会抛出异常,而remove两个元素却抛出异常呢?

我们来反推一下,抛出ConcurrentModificationException异常是在checkForComodification()校验中,而checkForComodification()校验是在next()方法中调用的,而想要调用next()方法,就需要hasNext()方法返回true,即迭代器认为还有下一个元素。

所以,我们已经可以猜到了,循环到倒数第二个元素时remove一个元素,接下来hasNext()方法返回了false,即判断已经没有下一个元素了,所以不会再调用next()方法,也就不会再做checkForComodification()校验,所以不会抛出异常;而remove两个元素后执行hasNext()方法返回了true,即判断还有下一个元素,所以继续调用next()方法,继续调用checkForComodification()校验,进而抛出异常。

上面是我们通过反推后得出的猜想,那么到底是不是这个原因呢?接下来就看一下hasNext()方法的具体实现。
在这里插入图片描述
可以看到hasNext()方法的实现很简单,仅仅是判断 cursor 和 size 是否相等,不相等即认为还有下一个元素,只有相等时才认为没有下一个元素了。

其中,size大家都懂,表示的是ArrayList包含的元素的实际个数。cursor表示的是 index of next element to return,我们可以简单理解为迭代器指向的下一个元素的下标(简单理解)。

理解了 cursor 和 size 代表的含义后,我们来举个栗子:

List<String> list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");
    
    for (String item : list) {
      if ("4".equals(item)) {   // cursor == 4
        list.remove("4");       // size == 4
      }
    }

当迭代到倒数第二个元素时,cursor等于4,当执行完 list.remove(“4”); 后,size等于4。接下来hasNext()方法返回false,迭代结束,没有抛出异常。

List<String> list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");
    
    for (String item : list) {
      if ("4".equals(item)) {   // cursor == 4
        list.remove("4");       // size == 4
        list.remove("5");       // size == 3
      }
    }

当迭代到倒数第二个元素时,cursor等于4,当执行完 list.remove(“4”); 后,size等于4,而执行完 list.remove(“5”); 后,size等于3。接下来hasNext()方法返回true,继续迭代,调用next()方法,调用checkForComodification()校验,抛出异常。

至此,原博文中作者在文末提出的问题回答完毕。

接下来,我们再回头看看原博文中作者曾提到的一个规律
在这里插入图片描述
因为这个规律只是原博文的作者根据他设计的小实验的结果提出的,所以这个规律其实并不太准确。现在我们再来继续完善一下这个规律。

通过前面的分析,我们已经知道在foreach循环ArrayList时调用remove方法后不抛异常的条件是,不能继续进行迭代,即hasNext()方法返回false,就不会再调用next()方法,不会调用checkForComodification()校验,不会抛出ConcurrentModificationException异常。也就是说,只要保证调用remove方法后,size == cursor 就可以了。我们再做一个实验,迭代到倒数第三个元素时,remove掉两个元素看看会不会抛出异常。

List<String> list = new ArrayList<>();
    list.add("1");
    list.add("2");
    list.add("3");
    list.add("4");
    list.add("5");
    
    for (String item : list) {
      if ("3".equals(item)) {   // cursor == 3
        list.remove("4");       // size == 4
        list.remove("5");       // size == 3
      }
    }

运行代码后发现没有抛出异常。

现在,我们可以重新描述一下我们发现的规律了:当迭代到倒数第n个元素时,remove掉n-1个元素,就不会抛出异常。

以上就是foreach循环ArrayList时调用remove方法的全部分析,以上提到的remove方法均为boolean java.util.List.remove(Object o)。

至于foreach循环ArrayList时调用add方法就没什么好分析的了,肯定会抛出异常,现在大家应该想一想就知道原因了。

最后,也是最重要的:
在这里插入图片描述
虽然在满足一定条件的情况下foreach循环ArrayList时调用remove方法不会抛出异常,但是这毕竟是一个坑,大家还是不要给自己挖坑了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章