Удалите элементы из HashSet во время итерации

Итак, если я попытаюсь удалить элементы из Java HashSet во время итерации, я получу ConcurrentModificationException. Как лучше всего удалить подмножество элементов из HashSet, как в следующем примере?

Set<Integer> set = new HashSet<Integer>();

for(int i = 0; i < 10; i++)
    set.add(i);

// Throws ConcurrentModificationException
for(Integer element : set)
    if(element % 2 == 0)
        set.remove(element);

Вот решение, но я не думаю, что оно очень элегантное:

Set<Integer> set = new HashSet<Integer>();
Collection<Integer> removeCandidates = new LinkedList<Integer>();

for(int i = 0; i < 10; i++)
    set.add(i);

for(Integer element : set)
    if(element % 2 == 0)
        removeCandidates.add(element);

set.removeAll(removeCandidates);

Спасибо!


person Community    schedule 10.07.2009    source источник


Ответы (7)


Вы можете вручную перебирать элементы набора:

Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
    Integer element = iterator.next();
    if (element % 2 == 0) {
        iterator.remove();
    }
}

Вы часто будете видеть этот шаблон с использованием for цикла, а не while цикла:

for (Iterator<Integer> i = set.iterator(); i.hasNext();) {
    Integer element = i.next();
    if (element % 2 == 0) {
        i.remove();
    }
}

Как отмечали люди, использование цикла for является предпочтительным, поскольку он сохраняет переменную итератора (в данном случае i) ограниченной областью меньшего размера.

person Adam Paynter    schedule 10.07.2009
comment
Я предпочитаю for while, но каждому свое. - person Tom Hawtin - tackline; 10.07.2009
comment
Я сам тоже пользуюсь for. Я использовал while, чтобы, надеюсь, сделать пример более понятным. - person Adam Paynter; 10.07.2009
comment
Я предпочитаю for в основном потому, что в этом случае переменная итератора ограничена рамками цикла. - person Kathy Van Stone; 10.07.2009
comment
Если используется while, то область действия итератора больше, чем должна быть. - person Steve Kuo; 10.07.2009
comment
Я предпочитаю время, потому что мне оно кажется чище. Объем итератора не должен быть проблемой, если вы факторизуете свой код. См. Книгу Бекс «Разработка через тестирование» или «Рефакторинг Фаулера» для получения дополнительной информации о факторинге кода. - person nash; 04.11.2009
comment
не будет ли это исключение ConcurrentModificationexception? - person Sumit Kumar Saha; 11.12.2018
comment
@SumitKumarSaha, нет, этого не должно быть, если вы изменяете коллекцию с помощью итератора, как это сделано в обоих примерах здесь. Если другая сторона изменила коллекцию во время итерации, или если вы изменили ее вне итератора, поведение не определено: javadoc - person chaserb; 24.04.2020

Причина, по которой вы получаете ConcurrentModificationException, заключается в том, что запись удаляется с помощью Set.remove (), а не Iterator.remove (). Если запись удаляется с помощью Set.remove () во время выполнения итерации, вы получите исключение ConcurrentModificationException. С другой стороны, в этом случае поддерживается удаление записей с помощью Iterator.remove () во время итерации.

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

Если вам нужно удалить запись во время итерации, вам нужно использовать длинную форму, которая напрямую использует Iterator.

for (Iterator<Integer> it = set.iterator(); it.hasNext();) {
    Integer element = it.next();
    if (element % 2 == 0) {
        it.remove();
    }
}
person sjlee    schedule 12.07.2009
comment
@ Разве ваш код не должен называть it.next ()? - person saurabheights; 01.06.2017
comment
Спасибо за это. Фиксированный. - person sjlee; 02.06.2017
comment
В какой момент создается экземпляр element? - person Phil Freihofner; 20.02.2018
comment
Фу. Фиксированный. Спасибо. - person sjlee; 21.02.2018

В Java 8 Collection есть хороший метод removeIf, который упрощает и делает работу безопаснее. Из документации API:

default boolean removeIf(Predicate<? super E> filter)
Removes all of the elements of this collection that satisfy the given predicate. 
Errors or runtime exceptions thrown during iteration or by the predicate 
are relayed to the caller.

Интересное примечание:

The default implementation traverses all elements of the collection using its iterator(). 
Each matching element is removed using Iterator.remove().

От: https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html#removeIf-java.util.function.Predicate-

person risoldi    schedule 24.08.2016
comment
Пример: integerSet.removeIf(integer-> integer.equals(5)); - person Jelle den Burger; 03.03.2017

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

Set<Integer> set = new HashSet<Integer>();
Collection<Integer> removeCandidates = new LinkedList<Integer>(set);

for(Integer element : set)
   if(element % 2 == 0)
       removeCandidates.add(element);

set.removeAll(removeCandidates);
person dfa    schedule 10.07.2009
comment
Я бы не рекомендовал это, поскольку это вводит скрытую временную связь. - person Romain F.; 04.03.2014
comment
@RomainF. - Что вы имеете в виду под скрытой временной связью? Вы имеете в виду потокобезопасность? Во-вторых, я бы тоже этого не рекомендовал, но у решения есть свои плюсы. Очень легко читать и, следовательно, поддерживать. - person saurabheights; 01.06.2017
comment
Да, цикл for дает побочный эффект, но я согласен, что это может быть наиболее читаемое решение, если вы не используете Java 8. В противном случае просто используйте метод removeIf. - person Romain F.; 01.06.2017
comment
Я думаю, что в этом ответе упускается из виду, что первый цикл был там только для того, чтобы иметь HashSet, из которого можно удалить определенные элементы. - person KeithWM; 11.07.2017

Как сказал Тимбер: «В Java 8 Collection есть хороший метод под названием removeIf, который упрощает и делает работу безопаснее».

Вот код, решающий вашу проблему:

set.removeIf((Integer element) -> {
    return (element % 2 == 0);
});

Теперь ваш набор содержит только нечетные значения.

person Getriax    schedule 05.04.2017

Вот более современный подход к потокам:

myIntegerSet.stream().filter((it) -> it % 2 != 0).collect(Collectors.toSet())

Однако это создает новый набор, поэтому ограничения памяти могут быть проблемой, если это действительно огромный набор.

РЕДАКТИРОВАТЬ: предыдущая версия этого ответа предлагала Apache CollectionUtils, но это было до появления паров.

person dustmachine    schedule 10.07.2009
comment
Этот ответ действительно начинает показывать свой возраст ... Теперь это можно сделать с помощью Java-8, который, возможно, стал чище. - person dustmachine; 16.06.2016
comment
Есть ли лучший метод для использования, или вы просто имели в виду возможность использовать лямбда вместо анонимного внутреннего класса? - person QWERTY; 25.01.2021
comment
Вот более современный способ: myIntegerSet.stream().filter((it) -> it % 2 != 0).collect(Collectors.toSet()) - person dustmachine; 27.01.2021

Другое возможное решение:

for(Object it : set.toArray()) { /* Create a copy */
    Integer element = (Integer)it;
    if(element % 2 == 0)
        set.remove(element);
}

Or:

Integer[] copy = new Integer[set.size()];
set.toArray(copy);

for(Integer element : copy) {
    if(element % 2 == 0)
        set.remove(element);
}
person alex2k8    schedule 21.02.2010
comment
Это (или создание ArrayList из набора) - лучшее решение, если вам случится не только удалить существующие элементы, но и добавить новые в набор во время цикла. - person Giulio Piancastelli; 07.03.2016