web-dev-qa-db-fra.com

Supprimer des éléments d'un hachage en itérant

Donc, si j'essaie de supprimer des éléments d'un Java HashSet en itérant, j'obtiens une ConcurrentModificationException. Quel est le meilleur moyen de supprimer un sous-ensemble des éléments d'un HashSet comme dans l'exemple suivant?

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

Voici une solution, mais je ne pense pas que ce soit très élégant:

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

Merci!

111
Scott

Vous pouvez parcourir manuellement les éléments de l'ensemble:

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

Vous verrez souvent ce modèle en utilisant une boucle for plutôt qu'une boucle while:

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

Comme on l'a déjà souligné, l'utilisation d'une boucle for est préférable car elle maintient la variable d'itérateur (i dans ce cas) confinée à une portée plus petite.

177
Adam Paynter

La raison pour laquelle vous obtenez un ConcurrentModificationException est qu’une entrée est supprimée via Set.remove () par opposition à Iterator.remove (). Si une entrée est supprimée via Set.remove () lorsqu'une itération est en cours, vous obtiendrez une ConcurrentModificationException. D'autre part, la suppression d'entrées via Iterator.remove () alors que l'itération est prise en charge dans ce cas.

La nouvelle boucle for est Nice, mais malheureusement cela ne fonctionne pas dans ce cas, car vous ne pouvez pas utiliser la référence Iterator.

Si vous devez supprimer une entrée lors de l'itération, vous devez utiliser le formulaire long qui utilise directement l'Iterator.

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

Java 8 Collection utilise une méthode Nice, appelée removeIf, qui rend les choses plus simples et plus sûres. À partir de la documentation de l'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.

Note intéressante:

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

De: https://docs.Oracle.com/javase/8/docs/api/Java/util/Collection.html#removeIf-Java.util.function.Predicate-

10
risoldi

vous pouvez également refactoriser votre solution en supprimant la première boucle:

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

Comme dit Timberwood - "Java 8 Collection utilise une méthode de Nice appelée removeIf qui rend les choses plus simples et plus sûres"

Voici le code qui résout votre problème:

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

Maintenant, votre ensemble ne contient que des valeurs impaires.

8
Getriax

A-t-il besoin d'être itératif? Si vous ne faites que filtrer ou sélectionner, je vous suggère d’utiliser Apache Commons CollectionUtils . Il existe quelques outils puissants et cela rend votre code "plus cool".

Voici une implémentation qui devrait fournir ce dont vous avez besoin:

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

Si vous utilisez fréquemment le même type de prédicat, vous pouvez l'extraire dans une variable statique afin de le réutiliser ... nommez-le sous la forme EVEN_NUMBER_PREDICATE. Certains peuvent voir ce code et le déclarer "difficile à lire", mais cela a l'air plus net lorsque vous extrayez le prédicat en statique. Ensuite, il est facile de voir que nous faisons une CollectionUtils.filter(...) et que cela me semble plus lisible (pour moi) qu’un tas de boucles partout dans la création.

4
dustmachine

Une autre solution possible:

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

Ou:

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

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