Как изменить коллекцию во время ее обработки
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как изменить коллекцию во время её обработки
Это один из классических"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
- По умолчанию используй
removeIf()— чистый, безопасный, функциональный - Для сложной логики — Iterator — явный контроль
- Для функционального кода — Stream API — современный подход
- Избегай прямых вызовов remove() на коллекции в for-each циклах
- Для многопоточных сценариев — concurrent классы или synchronized блоки
Заключение
Главный вывод: никогда не модифицируй коллекцию напрямую во время итерации. Используй Iterator, removeIf() или создавай новую коллекцию. Это избежит ConcurrentModificationException и сделает код более безопасным и читаемым.