← Назад к вопросам

Как изменить коллекцию во время ее обработки

2.0 Middle🔥 141 комментариев
#Коллекции

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как изменить коллекцию во время её обработки

Это один из классических"gotchas" в Java. Попытка изменить коллекцию во время итерации может привести к ConcurrentModificationException. Рассмотрим проблему и несколько надежных решений.

Проблема: ConcurrentModificationException

// ❌ НЕПРАВИЛЬНО - выбросит исключение
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));

for (String item : list) {
    System.out.println(item);
    if (item.equals("B")) {
        list.remove("B");  // ConcurrentModificationException!
    }
}

Причина: Iterator поддерживает внутренний счётчик модификаций (modCount). Когда коллекция изменяется напрямую (не через iterator.remove()), iterator обнаруживает это и выбрасывает исключение.

Решение 1: Используй Iterator.remove()

Это самый чистый и рекомендуемый способ:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));

// Правильный способ
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    System.out.println(item);
    
    if (item.equals("B")) {
        iterator.remove();  // Безопасное удаление
    }
}
// Результат: [A, C, D]
System.out.println(list);

Преимущества:

  • Безопасно и идиоматично
  • Работает с any Collection
  • Перехватывает исключения на уровне iterator
// Более сложный пример - удаление по условию
List<User> users = new ArrayList<>(Arrays.asList(
    new User("John", 30),
    new User("Jane", 25),
    new User("Bob", 35)
));

Iterator<User> it = users.iterator();
while (it.hasNext()) {
    User user = it.next();
    if (user.getAge() < 30) {
        it.remove();
    }
}
// Остались только пользователи старше 30

Решение 2: removeIf() (Java 8+)

Это функциональный подход, удаляет элементы по предикату:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));

// Удали все элементы, которые содержат "B"
list.removeIf(item -> item.equals("B"));
System.out.println(list);  // [A, C, D]

Пример с объектами:

List<User> users = new ArrayList<>(Arrays.asList(
    new User("John", 30),
    new User("Jane", 25),
    new User("Bob", 35)
));

// Удали всех пользователей младше 30
users.removeIf(user -> user.getAge() < 30);

Преимущества:

  • Компактный и читаемый код
  • Потокобезопасен внутри removeIf()
  • Работает с любыми коллекциями

Решение 3: Создай новую коллекцию

Этот подход идеален для Stream API:

List<String> originalList = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));

// Создай новый список без "B"
List<String> filteredList = originalList.stream()
    .filter(item -> !item.equals("B"))
    .collect(Collectors.toList());

System.out.println(filteredList);  // [A, C, D]

Пример с более сложной логикой:

List<User> users = new ArrayList<>(Arrays.asList(
    new User("John", 30),
    new User("Jane", 25),
    new User("Bob", 35),
    new User("Alice", 28)
));

// Получи пользователей старше 26 лет
List<User> filtered = users.stream()
    .filter(user -> user.getAge() >= 26)
    .collect(Collectors.toList());

// Или для update исходного списка
users = filtered;

Преимущества:

  • Функциональный подход
  • Чистая логика
  • Легко комбинировать с другими операциями

Решение 4: Копирование и итерация

Иногда нужно итерировать оригинальный список и модифицировать его:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));

// Создай копию для итерации
List<String> copy = new ArrayList<>(list);

for (String item : copy) {
    System.out.println(item);
    if (item.equals("B")) {
        list.remove(item);  // Теперь безопасно модифицировать оригинал
    }
}
System.out.println(list);  // [A, C, D]

Когда это полезно:

  • Нужно изменять и читать из одного списка одновременно
  • Простая и понятная логика

Решение 5: forEach с условной логикой

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "B"));
String toRemove = "B";

// Двухшаговый процесс
List<String> toDelete = new ArrayList<>();
list.forEach(item -> {
    if (item.equals(toRemove)) {
        toDelete.add(item);
    }
});
list.removeAll(toDelete);

System.out.println(list);  // [A, C, D]

Решение 6: Для Map коллекций

Для Map, ConcurrentHashMap обычно безопаснее:

// ❌ Может вызвать ConcurrentModificationException
Map<String, Integer> map = new HashMap<>(Map.of(
    "A", 1,
    "B", 2,
    "C", 3
));

for (String key : new ArrayList<>(map.keySet())) {
    if (key.equals("B")) {
        map.remove(key);
    }
}

Правильно:

Map<String, Integer> map = new HashMap<>(Map.of(
    "A", 1,
    "B", 2,
    "C", 3
));

// Способ 1: Копия ключей
for (String key : new ArrayList<>(map.keySet())) {
    if (key.equals("B")) {
        map.remove(key);
    }
}

// Способ 2: Iterator
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, Integer> entry = it.next();
    if (entry.getKey().equals("B")) {
        it.remove();
    }
}

// Способ 3: removeIf (для Java 8+)
map.keySet().removeIf(key -> key.equals("B"));

Решение 7: Многопоточная обработка

Для многопоточных сценариев используй ConcurrentHashMap или CopyOnWriteArrayList:

import java.util.concurrent.CopyOnWriteArrayList;

List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C", "D"));

// Thread-safe итерация и модификация
for (String item : list) {
    if (item.equals("B")) {
        list.remove(item);  // Безопасно в многопоточной среде
    }
}

Когда использовать:

  • Доступ из нескольких потоков
  • Производительность важна (читает >= пишет)
  • CopyOnWriteArrayList создает copy при каждом изменении

Сравнительная таблица решений

РешениеКогда использоватьПроизводительностьПотокобезопасность
Iterator.remove()Удаление элементовХорошоSingle-threaded
removeIf()Фильтрация по предикатуОтличноSingle-threaded
Stream APIНовая коллекция нужнаХорошоSingle-threaded
КопированиеПростая логикаСреднееSingle-threaded
CopyOnWriteArrayListМногопоток, мало writesСреднееMulti-threaded
ConcurrentHashMapМногопоток для MapОтличноMulti-threaded

Best Practices

  1. По умолчанию используй removeIf() — чистый, безопасный, функциональный
  2. Для сложной логики — Iterator — явный контроль
  3. Для функционального кода — Stream API — современный подход
  4. Избегай прямых вызовов remove() на коллекции в for-each циклах
  5. Для многопоточных сценариев — concurrent классы или synchronized блоки

Заключение

Главный вывод: никогда не модифицируй коллекцию напрямую во время итерации. Используй Iterator, removeIf() или создавай новую коллекцию. Это избежит ConcurrentModificationException и сделает код более безопасным и читаемым.