为什么列表的反向子列表的 List.addAll 会导致 ConcurrentModificationException

Why does List.addAll of a reversed subList of the list cause a ConcurrentModificationException

我一直在尝试将 sub list of a list, reverse it, and place the reversed 列表放回起始位置。例如,假设我们有列表 [1, 2, 3, 4, 5, 6],那么从索引 2 反转到索引 4 将得到 [1, 2, 5, 4, 3, 6].

我已经为此编写了一些代码,但它每次都会给出一个 ConcurrentModificationException(除非 startIndex == endIndex)。下面提供了一个最小的可重现示例:

int startIndex = 2;
int endIndex = 4;
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);

List<Integer> toReverse = list.subList(startIndex, endIndex+1);
Collections.reverse(toReverse);
list.removeAll(toReverse);
list.addAll(startIndex, toReverse);

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(Unknown Source)
at java.util.ArrayList$SubList.size(Unknown Source) at
java.util.AbstractCollection.toArray(Unknown Source) at
java.util.ArrayList.addAll(Unknown Source) at
test.ConcurrentExample.main(ConcurrentExample.java:64)

错误所指的实际行是list.addAll(startIndex, toReverse);

我不确定问题出在哪里,因为在迭代时似乎没有任何变化。如果有人能解释 为什么 会发生这种情况 and/or 如何 修复它,将不胜感激。

根据 helospark & Nir Levy 的建议,在 Stream

中使用 skip & limit
List<Integer> toReverse = list.stream() //
                .skip(startIndex) //
                .limit(endIndex + 1) //
                .collect(Collectors.toList());

List.subList returns 指定元素之间列表的实时视图,而不是那些元素的副本(参见 documentation),因此添加到原始列表也会修改子列表,这将导致 ConcurrentModificationException(因为正在添加的内容和您添加的内容也会同时修改)。

list.subList(startIndex, endIndex+1)

您可以通过复制列表来修复代码,例如

List<Integer> toReverse = new ArrayList<>(list.subList(startIndex, endIndex+1));

来自 ArrayList.subList 的文档:

The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa

因此,当您尝试在子列表 'view' 的索引处添加项目时,它会创建并发修改。

问题出在ArrayList#checkForComodification

private void checkForComodification() {
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
    }
}

但是在这种特殊情况下,您不需要手动重新添加反向子列表,因为反向是在 原始 列表上执行的。所以你只需要放下

list.removeAll(...);
list.addAll(...);

只留下这段代码:

List<Integer> toReverse = list.subList(startIndex, endIndex+1);
Collections.reverse(toReverse);