Удаление элементов из 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);

Спасибо!

Ответ 1

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

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 в этом случае) ограничивается меньшей областью.

Ответ 2

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

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

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

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

Ответ 3

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

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);

Ответ 4

Нужно ли во время итерации? Если все, что вы делаете, это фильтрация или выбор, я бы предложил использовать Apache Commons CollectionUtils. Там есть несколько мощных инструментов, и это делает ваш код "более холодным".

Здесь реализована реализация, которая должна обеспечить то, что вам нужно:

Set<Integer> myIntegerSet = new HashSet<Integer>();
// Integers loaded here
CollectionUtils.filter( myIntegerSet, new Predicate() {
                              public boolean evaluate(Object input) {
                                  return (((Integer) input) % 2 == 0);
                              }});

Если вы часто используете тот же предикат, вы можете вытащить его в статическую переменную для повторного использования... назовите это что-то вроде EVEN_NUMBER_PREDICATE. Некоторые могут увидеть этот код и объявить его "трудным для чтения", но он выглядит более чистым, когда вы вытаскиваете Predicate в статику. Тогда легко увидеть, что мы делаем CollectionUtils.filter(...) и это кажется более читаемым (для меня), чем пучок циклов по всему созданию.

Ответ 5

В 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-

Ответ 6

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

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

Или:

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

for(Integer element : copy) {
    if(element % 2 == 0)
        set.remove(element);
}

Ответ 7

Как сказал пилот - "Java 8 Collection имеет хороший метод под названием removeIf, который упрощает и упрощает"

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

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

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