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

Спасибо!

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

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

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

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

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

В 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

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

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

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

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

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

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

Это должно быть во время итерации? Если все, что вы делаете, это фильтрация или выбор, я бы предложил использовать 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, Некоторые могут увидеть этот код и объявить его "трудночитаемым", но он выглядит чище, когда вы выводите Предикат в статическое состояние. Тогда легко увидеть, что мы делаем CollectionUtils.filter(...) и это кажется более читабельным (для меня), чем куча циклов по всему творению.

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

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);
}
Другие вопросы по тегам