6

Вызов метода remove в цикле foreach в Java

1

Проблема с удалением элементов из коллекции в 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 ответ(ов)

0

В вашем коде вы создаете клон списка 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(); // Удаляем элемент безопасно через итератор
    }
}

Таким образом, вы избегете проблем с изменением структуры списка во время итерации и сделаете код более эффективным.

0

Дизайн "улучшенного for-цикла" в Java был разработан с целью не показывать итератор коду, но единственный способ безопасно удалить элемент — это обратиться к итератору. Поэтому в данном случае нужно делать это по-старинке:

for (Iterator<String> i = names.iterator(); i.hasNext();) {
    String name = i.next();
    // Выполнить что-то
    i.remove();
}

Если в вашем реальном коде использование улучшенного for-цикла действительно необходимо, вы можете добавить элементы в временную коллекцию и затем вызвать removeAll на основном списке после цикла.

EDIT (добавление): Нет, изменение списка каким-либо образом вне метода iterator.remove() во время итерации приведет к ошибкам. Единственный способ обойти это — использовать CopyOnWriteArrayList, но он в основном предназначен для решения задач, связанных с конкурентным доступом.

Наиболее простой (по количеству строк кода) способ удалить дубликаты — это поместить список в LinkedHashSet (и затем обратно в List, если это необходимо). Это сохраняет порядок вставки, одновременно удаляя дубликаты.

0

Вам не нужно знать об итераторах, чтобы удалять элементы из списка внутри цикла. Вот что я делал до сегодняшнего дня:

List<String> names = .... 
for (int i = names.size() - 1; i >= 0; i--) {    
    // Выполняем что-то    
    names.remove(i);
} 

Этот способ всегда срабатывает и может использоваться в других языках или структурах данных, которые не поддерживают итераторы.

Удаление элементов с конца списка (например, начиная с names.size() - 1 и двигаясь к началу) предотвращает проблемы, связанные с изменением размера списка во время итерации, что может привести к исключениям или пропуску элементов. Так что ваш подход вполне корректен и работает в Java, а также может быть применим к другим языкам программирования.

0

Да, вы можете использовать цикл 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.

0

Перевод ответа в стиле StackOverflow.com:


Да, вы абсолютно правы в том, что ваш подход является более "инклюзивным" по сравнению с "эксклюзивным". В вашем примере вместо того чтобы удалять элементы, которые не удовлетворяют условиям, вы создаете новый список, в который помещаете только те элементы, которые соответствуют критериям. Это более читаемо и предотвращает потенциальные ошибки, связанные с изменением списка во время его итерации.

Ранее используемый вами код с индексами, который удаляет элементы из списка, может приводить к проблемам, когда индексы сдвигаются после удаления. В результате можно пропустить элементы или получить исключение. Ваш новый подход с использованием for-each заметно упрощает эту логику, делая ее более безопасной.

Относительно вопроса, когда "remove" может быть предпочтительнее: если у вас очень большой список, а количество элементов, которое необходимо удалить, значительно меньше общего числа элементов, то возможно, имеет смысл использовать remove. Так вы избежите ненужных операций добавления в новый список. Однако в вашем случае, если разница в количестве удаляемых и добавляемых элементов незначительна, как вы сами упомянули, то выбранный вами метод с использованием permittedServices предпочтителен как с точки зрения читаемости, так и избегания ошибок.

Таким образом, ваш рефакторинг выглядит к месту, и это, безусловно, улучшит поддержку и понимание кода.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь