为何ArrayList一级子列表的迭代器next()会抛出ConcurrentModificationException?
好问题!让我们深入拆解这段代码背后的逻辑,搞清楚这个额外检查的必要性。
首先得明确SubList的本质:它不是一个独立的集合,而是原ArrayList的视图——所有操作最终都会映射到原列表的底层数组elementData上。这里的offset是子列表在原数组中的起始偏移索引,SubList.this.size是子列表包含的元素个数。
现在看触发异常的条件:offset + i >= elementData.length。先梳理变量含义:
i是当前迭代器的cursor位置(初始为0,每次调用next后自增1)- 前面已经检查过
i >= SubList.this.size会抛出NoSuchElementException,所以正常情况下i的范围是0 <= i < SubList.size
正常来说,offset + i应该小于offset + SubList.size,而原列表的elementData长度肯定能覆盖这个范围(毕竟子列表是原列表的一部分)。那什么时候这个条件会成立?
核心原因:原ArrayList的底层数组失效,子列表索引范围不再合法
虽然checkForComodification()已经在检查原列表的修改次数(modCount),但这个额外条件是一种防御性的快速失败补充,针对几种极端但可能存在的场景:
原列表被缩容到子列表范围之外
比如原ArrayList有10个元素,子列表从索引5开始(offset=5),包含5个元素(SubList.size=5)。如果原列表先删除了后面的元素,实际size变为6,再调用trimToSize(),elementData会被缩容到长度6。此时offset + i(比如i=4时,5+4=9)就会大于新的elementData.length=6,子列表的视图已经完全失效。极端场景下原数组被意外替换
虽然ArrayList的elementData是私有变量,但在某些特殊情况(比如反射修改、内部类非常规操作)下,原列表的elementData可能被替换成更短的数组。这时候即使modCount没变化(这种情况非常罕见,但代码做了兜底),这个条件也能检测到子列表的索引已经超出新数组范围,及时抛出异常。
为什么不只依赖checkForComodification()?
你可能会问:checkForComodification()不是已经在检查modCount了吗?大部分修改原列表的操作都会递增modCount(比如add、remove、trimToSize等),这时候checkForComodification()会先抛出异常。但这个额外条件是为了覆盖那些modCount没变化但数组长度失效的极端场景,确保迭代器的快速失败特性万无一失——毕竟一旦子列表索引超出原数组长度,继续访问会导致数组越界,而抛出ConcurrentModificationException比ArrayIndexOutOfBoundsException更能准确反映“视图失效/并发修改”的问题。
简单来说,这个检查是在说:“哪怕修改次数没变化,但如果原数组已经装不下子列表的元素了,说明子列表的视图已经彻底无效,必须终止迭代并抛出异常。”
内容的提问来源于stack exchange,提问作者Farhan stands with Palestine

