Избежание исключения ConcurrentModificationException в Java

1. Вступление

В этой статье мы рассмотрим класс ConcurrentModificationException .

Сначала мы объясним, как это работает, а затем докажем это с помощью теста для его запуска.

Наконец, мы попробуем некоторые обходные пути, используя практические примеры.

2. Инициирование ConcurrentModificationException

По сути, ConcurrentModificationException используется для быстрого отказа, когда что-то, что мы повторяем, модифицируется. Давайте докажем это с помощью простого теста:

@Test(expected = ConcurrentModificationException.class)
public void whilstRemovingDuringIteration__shouldThrowException() throws InterruptedException {

    List<Integer> integers = newArrayList(1, 2, 3);

    for (Integer integer : integers) {
        integers.remove(1);
    }
}

Как мы видим, перед завершением нашей итерации мы удаляем элемент. Вот что вызывает исключение.

3. Решения

Иногда нам может понадобиться удалить элементы из коллекции во время итерации. Если это так, то есть несколько решений.

3.1. Использование итератора напрямую

Цикл for-each использует Iterator за кулисами, но менее многословен. Однако, если мы реорганизовали наш предыдущий тест для использования Iterator, у нас будет доступ к дополнительным методам, таким как remove () . Давайте попробуем использовать этот метод для изменения нашего списка вместо этого:

for (Iterator<Integer> iterator = integers.iterator(); iterator.hasNext();) {
    Integer integer = iterator.next();
    if(integer == 2) {
        iterator.remove();
    }
}

Теперь мы заметим, что нет никаких исключений. Причина этого заключается в том, что метод remove () не вызывает ConcurrentModificationException. Безопасно вызывать во время итерации.

3.2. Не удаляется во время итерации

Если мы хотим сохранить наш цикл for-each , то мы можем. Нам просто нужно подождать после итерации, прежде чем мы удалим элементы. Давайте попробуем это, добавив то, что мы хотим удалить, в список toRemove во время итерации:

List<Integer> integers = newArrayList(1, 2, 3);
List<Integer> toRemove = newArrayList();

for (Integer integer : integers) {
    if(integer == 2) {
        toRemove.add(integer);
    }
}
integers.removeAll(toRemove);

assertThat(integers).containsExactly(1, 3);

Это еще один эффективный способ обойти проблему.

3.3. Использование removeIf ()

Java 8 представила метод removeIf () для интерфейса Collection .

Это означает, что если мы работаем с ним, мы можем использовать идеи функционального программирования для достижения тех же результатов снова:

List<Integer> integers = newArrayList(1, 2, 3);

integers.removeIf(i -> i == 2);

assertThat(integers).containsExactly(1, 3);

Этот декларативный стиль предлагает нам наименьшее количество многословия. Однако, в зависимости от варианта использования, мы можем найти другие методы более удобными.

3.4. Фильтрация с использованием потоков

Погружаясь в мир функционального/декларативного программирования, мы можем забыть о мутирующих коллекциях, вместо этого мы можем сосредоточиться на элементах, которые должны быть фактически обработаны:

Collection<Integer> integers = newArrayList(1, 2, 3);

List<String> collected = integers
  .stream()
  .filter(i -> i != 2)
  .map(Object::toString)
  .collect(toList());

assertThat(collected).containsExactly("1", "3");

Мы сделали обратное нашему предыдущему примеру, предоставив предикат для определения элементов для включения, а не исключения. Преимущество состоит в том, что мы можем связать воедино другие функции наряду с удалением. В этом примере мы используем функционал __map (), но можем использовать еще больше операций, если захотим.

4. Заключение

В этой статье мы показали проблемы, с которыми вы можете столкнуться, если вы удаляете элементы из коллекции во время итерации, а также предоставили некоторые решения, чтобы устранить проблему.

Реализация этих примеров может быть найдена на over на GitHub . Это проект Maven, поэтому его легко запустить как есть.