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

Как можно обойти коллекцию и изменить ее без ошибок?

1.3 Junior🔥 111 комментариев
#Коллекции#Основы Java

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

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

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

# Обход коллекции с изменением её содержимого без ошибок

Одна из распространенных ошибок в Java — попытка изменить коллекцию во время её обхода. Это приводит к ConcurrentModificationException. Вот различные способы безопасного обхода и модификации коллекций.

Проблема: ConcurrentModificationException

Неправильный способ:

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"));

for (String name : names) {
    if (name.startsWith("П")) {
        names.remove(name); // ❌ ConcurrentModificationException!
    }
}

Почему это происходит: iterator внутри for-each цикла проверяет, не изменилась ли коллекция. Если изменилась — выбрасывает исключение.

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

Это правильный способ удалить элемент во время обхода:

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"));

Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    String name = iterator.next();
    if (name.startsWith("П")) {
        iterator.remove(); // ✅ Безопасно удаляет текущий элемент
    }
}

System.out.println(names); // [Иван, Мария]

Как это работает:

  • Iterator.remove() удаляет элемент, который только что был возвращен next()
  • Iterator отслеживает это изменение и корректно обновляет свое состояние
  • Никакой ConcurrentModificationException

Решение 2: Создание нового списка

Когда нужны сложные условия, создай новый список:

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"));
List<String> filtered = new ArrayList<>();

for (String name : names) {
    if (!name.startsWith("П")) {
        filtered.add(name); // Добавляем в новый список
    }
}

names = filtered; // или names.clear(); names.addAll(filtered);
System.out.println(names); // [Иван, Мария]

Решение 3: Stream API (Java 8+)

Самый функциональный и читаемый способ:

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"));

// Filter and collect
List<String> filtered = names.stream()
    .filter(name -> !name.startsWith("П"))
    .collect(Collectors.toList());

System.out.println(filtered); // [Иван, Мария]

// Или заменить исходный список
names = filtered;

Решение 4: removeIf() метод (Java 8+)

Для простого удаления элементов:

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"));

// Удаляет элементы, для которых предикат возвращает true
names.removeIf(name -> name.startsWith("П"));

System.out.println(names); // [Иван, Мария]

Это лучше, чем ручной iterator, потому что:

  • Более читаемо
  • Безопасно
  • Оптимизировано для разных типов коллекций

Решение 5: Копирование списка перед обходом

Обход копии, изменение оригинала:

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"));

for (String name : new ArrayList<>(names)) { // Обходим копию
    if (name.startsWith("П")) {
        names.remove(name); // Изменяем оригинал
    }
}

System.out.println(names); // [Иван, Мария]

Минусы: создается дополнительная копия (неэффективно для больших списков).

Решение 6: Работа с Set'ами

Для Set'ов работает так же:

Set<String> names = new HashSet<>(Arrays.asList("Иван", "Петр", "Мария"));

// Iterator.remove()
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    if (iterator.next().startsWith("П")) {
        iterator.remove();
    }
}

// Или removeIf
names.removeIf(name -> name.startsWith("П"));

System.out.println(names); // [Иван, Мария]

Решение 7: Работа с Map'ами

Map<String, Integer> users = new HashMap<>();
users.put("Иван", 30);
users.put("Петр", 25);
users.put("Мария", 28);

// ❌ Неправильно
for (String key : users.keySet()) {
    if (key.startsWith("П")) {
        users.remove(key); // ConcurrentModificationException
    }
}

// ✅ Правильно с Iterator
Iterator<String> iterator = users.keySet().iterator();
while (iterator.hasNext()) {
    if (iterator.next().startsWith("П")) {
        iterator.remove();
    }
}

// ✅ Или removeIf
users.keySet().removeIf(key -> key.startsWith("П"));

System.out.println(users); // {Иван=30, Мария=28}

Решение 8: Синхронизированные коллекции

Если работаешь в многопоточной среде:

List<String> syncList = Collections.synchronizedList(
    new ArrayList<>(Arrays.asList("Иван", "Петр", "Мария"))
);

// Даже с synchronized, нужно использовать правильный паттерн
synchronized(syncList) { // Синхронизируем весь блок
    Iterator<String> iterator = syncList.iterator();
    while (iterator.hasNext()) {
        if (iterator.next().startsWith("П")) {
            iterator.remove();
        }
    }
}

Решение 9: CopyOnWriteArrayList (потокобезопасность)

Для многопоточных приложений:

List<String> names = new CopyOnWriteArrayList<>(
    Arrays.asList("Иван", "Петр", "Мария")
);

// Можно безопасно модифицировать во время обхода
for (String name : names) {
    if (name.startsWith("П")) {
        names.remove(name); // Безопасно в многопоточной среде
    }
}

System.out.println(names);

Как работает: при каждом изменении создается копия списка, а итераторы работают со старой версией.

Сравнение подходов

СпособБезопасностьПроизводительностьЧитаемостьКогда использовать
Iterator.remove()ХорошоСредняяПростой обход с удалением
removeIf()ХорошоОтличнаяПростые условия удаления
Stream APIХорошоОтличнаяСовременный Java код
Новый списокХорошоХорошаяСложная фильтрация
for new ArrayList⚠️ПлохоСредняяРедко, для простых случаев
CopyOnWriteArrayListПлохоХорошаяМногопоточные приложения

Лучшие практики

Используй removeIf() для удаления элементов — это стандартный способ

Используй Stream API для фильтрации и трансформаций

Используй Iterator.remove() если нужна логика, которую нельзя выразить в removeIf

Обрабатывай исключения при работе с множественными потоками

Не удаляй элементы напрямую через remove() в for-each цикле

Не изменяй индекс при обходе for с индексом

Пример: Удаление duplicates

List<String> names = new ArrayList<>(Arrays.asList("Иван", "Иван", "Петр", "Мария"));

// Способ 1: Stream
names = names.stream().distinct().collect(Collectors.toList());

// Способ 2: Set (теряется порядок)
names = new ArrayList<>(new LinkedHashSet<>(names));

// Способ 3: Iterator
Set<String> seen = new HashSet<>();
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    if (!seen.add(iterator.next())) {
        iterator.remove();
    }
}

System.out.println(names); // [Иван, Петр, Мария]

Заключение

Изменение коллекции во время обхода — частая ошибка. Используй:

  • removeIf() для удаления
  • Stream API для фильтрации
  • Iterator.remove() для сложных сценариев
  • CopyOnWriteArrayList для многопоточности

Это обеспечит безопасность и читаемость кода.