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,
у нас будет доступ к дополнительным методам, таким как 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. Использование удаленияЕсли()
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");
Мы сделали обратное нашему предыдущему примеру, предоставив предикат для определения элементов, которые следует включать, а не исключать. Преимущество в том, что мы можем объединить другие функции вместе с удалением. В примере мы используем функциональную карту(),
но можем использовать и больше операций, если захотим.
4. Вывод
В этой статье мы показали проблемы, с которыми вы можете столкнуться при удалении элементов из коллекции во время итерации, а также предоставили некоторые решения для устранения этой проблемы.
Реализацию этих примеров можно найти на GitHub . Это проект Maven, поэтому его легко запустить как есть.