Почему List.addAll обратного подсписка списка вызывает исключение ConcurrentModificationException

Я пытался взять подсписок списка, перевернуть его и поместить перевернутый список обратно в исходное положение. Например, скажем, у нас есть список [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);

Исключение в потоке "main" java.util.ConcurrentModificationException
  at java.util.ArrayList $ SubList.checkForComodification (неизвестный источник)
  на java.util.ArrayList $ SubList.size (неизвестный источник) на
java.util.AbstractCollection.toArray (Неизвестный источник) в
java.util.ArrayList.addAll (неизвестный источник) на
test.ConcurrentExample.main(ConcurrentExample.java:64)

Фактическая строка, на которую ссылается ошибка, - list.addAll(startIndex, toReverse);.

Я не уверен, в чем проблема, так как кажется, что ничего не меняется во время итерации. Если бы кто-нибудь мог объяснить, почему это происходит и/или как это исправить, это будет с благодарностью.

Ответы

Ответ 1

List.subList возвращает представление в реальном времени списка между указанными элементами, а не копию этих элементов (см. документацию), поэтому добавление в исходный список также изменит подсписок, что приведет к ConcurrentModificationException (поскольку то, что добавляется, и то, к чему вы добавляете, также изменяются одновременно).

list.subList(startIndex, endIndex+1)

Вы можете исправить код, скопировав список, например

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

Ответ 2

из документации ArrayList.subList :

Возвращенный список поддерживается этим списком, поэтому неструктурные изменения в возвращенный список отражается в этом списке, и наоборот

Поэтому, когда вы пытаетесь добавить элементы в индекс подсписка 'view', это создает одновременную модификацию.

Ответ 3

Проблема лежит здесь в 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);

Ответ 4

Из предложений helospark & Нир Леви, используйте Пропустить & ограничение в потоке

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