Вызов метода remove в цикле foreach в Java
Проблема с удалением элементов из коллекции в Java при итерации
В Java, легально ли вызывать метод remove
на коллекции, когда мы итерируемся по ней с использованием цикла foreach
? Например, следующий код вызывает у меня вопросы:
List<String> names = ....;
for (String name : names) {
// Выполняем какие-то действия
names.remove(name);
}
Также, является ли законным удаление элементов, которые еще не были обработаны в цикле? Например:
// Предположим, что в списке names есть дубликаты
List<String> names = ....;
for (String name : names) {
// Выполняем какие-то действия
while (names.remove(name));
}
Смогу ли я избежать исключения ConcurrentModificationException
, если воспользуюсь описанными подходами? Какие есть рекомендации по безопасному удалению элементов из коллекции во время итерации?
5 ответ(ов)
В вашем коде вы создаете клон списка names
и итераете по этому клону, в то время как удаляете элементы из оригинального списка. Это немного аккуратнее, чем в верхнем ответе. Однако следует учесть, что использование ArrayList<String>(names)
создает новый список, что может привести к излишнему потреблению памяти, если names
сильно велик.
Поскольку вы хотите избежать ConcurrentModificationException
, более подходящий вариант – использовать итератор. Вот пример:
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
// Делайте что-то с name
if (name.equals(nameToRemove)) {
iterator.remove(); // Удаляем элемент безопасно через итератор
}
}
Таким образом, вы избегете проблем с изменением структуры списка во время итерации и сделаете код более эффективным.
Дизайн "улучшенного for-цикла" в Java был разработан с целью не показывать итератор коду, но единственный способ безопасно удалить элемент — это обратиться к итератору. Поэтому в данном случае нужно делать это по-старинке:
for (Iterator<String> i = names.iterator(); i.hasNext();) {
String name = i.next();
// Выполнить что-то
i.remove();
}
Если в вашем реальном коде использование улучшенного for-цикла действительно необходимо, вы можете добавить элементы в временную коллекцию и затем вызвать removeAll
на основном списке после цикла.
EDIT (добавление): Нет, изменение списка каким-либо образом вне метода iterator.remove()
во время итерации приведет к ошибкам. Единственный способ обойти это — использовать CopyOnWriteArrayList
, но он в основном предназначен для решения задач, связанных с конкурентным доступом.
Наиболее простой (по количеству строк кода) способ удалить дубликаты — это поместить список в LinkedHashSet
(и затем обратно в List
, если это необходимо). Это сохраняет порядок вставки, одновременно удаляя дубликаты.
Вам не нужно знать об итераторах, чтобы удалять элементы из списка внутри цикла. Вот что я делал до сегодняшнего дня:
List<String> names = ....
for (int i = names.size() - 1; i >= 0; i--) {
// Выполняем что-то
names.remove(i);
}
Этот способ всегда срабатывает и может использоваться в других языках или структурах данных, которые не поддерживают итераторы.
Удаление элементов с конца списка (например, начиная с names.size() - 1
и двигаясь к началу) предотвращает проблемы, связанные с изменением размера списка во время итерации, что может привести к исключениям или пропуску элементов. Так что ваш подход вполне корректен и работает в Java, а также может быть применим к другим языкам программирования.
Да, вы можете использовать цикл for-each для этой задачи. Для этого вам нужно создать отдельный список, чтобы хранить удаляемые элементы, а затем удалить все элементы этого списка из исходного списка names
, используя метод removeAll()
.
Вот пример кода:
List<String> names = ...;
// Создаем отдельный список для хранения удаляемых элементов
List<String> toRemove = new ArrayList<String>();
for (String name : names) {
// Выполняем какие-то действия: условные проверки
// Например, если имя начинается с буквы 'A', добавляем его в toRemove
if (name.startsWith("A")) {
toRemove.add(name);
}
}
names.removeAll(toRemove);
// Теперь список names содержит ожидаемые значения
Этот подход позволяет избежать изменения списка names
во время итерации, что может привести к ConcurrentModificationException
.
Перевод ответа в стиле StackOverflow.com:
Да, вы абсолютно правы в том, что ваш подход является более "инклюзивным" по сравнению с "эксклюзивным". В вашем примере вместо того чтобы удалять элементы, которые не удовлетворяют условиям, вы создаете новый список, в который помещаете только те элементы, которые соответствуют критериям. Это более читаемо и предотвращает потенциальные ошибки, связанные с изменением списка во время его итерации.
Ранее используемый вами код с индексами, который удаляет элементы из списка, может приводить к проблемам, когда индексы сдвигаются после удаления. В результате можно пропустить элементы или получить исключение. Ваш новый подход с использованием for-each
заметно упрощает эту логику, делая ее более безопасной.
Относительно вопроса, когда "remove" может быть предпочтительнее: если у вас очень большой список, а количество элементов, которое необходимо удалить, значительно меньше общего числа элементов, то возможно, имеет смысл использовать remove
. Так вы избежите ненужных операций добавления в новый список. Однако в вашем случае, если разница в количестве удаляемых и добавляемых элементов незначительна, как вы сами упомянули, то выбранный вами метод с использованием permittedServices
предпочтителен как с точки зрения читаемости, так и избегания ошибок.
Таким образом, ваш рефакторинг выглядит к месту, и это, безусловно, улучшит поддержку и понимание кода.
Как выйти из вложенных циклов в Java?
В деталях: как работает цикл 'for each' в Java?
Цикл for для перебора enum в Java
PHP: Как определить первую и последнюю итерацию в цикле foreach?
Как создать утечку памяти в Java?