web-dev-qa-db-fra.com

Supprimer des éléments d'une collection dans Java tout en itérant dessus)

Je veux pouvoir supprimer plusieurs éléments d'un ensemble pendant que j'itère dessus. Au départ, j'espérais que les itérateurs étaient suffisamment intelligents pour que la solution naïve ci-dessous fonctionne.

Set<SomeClass> set = new HashSet<SomeClass>();
fillSet(set);
Iterator<SomeClass> it = set.iterator();
while (it.hasNext()) {
    set.removeAll(setOfElementsToRemove(it.next()));
}

Mais cela lance un ConcurrentModificationException.

Notez que iterator.remove () ne fonctionnera pas aussi loin que je peux voir car je dois supprimer plusieurs choses à la fois. Supposons également qu'il n'est pas possible d'identifier les éléments à supprimer "à la volée", mais qu'il est possible d'écrire la méthode setOfElementsToRemove(). Dans mon cas spécifique, il faudrait beaucoup de mémoire et de temps de traitement pour déterminer ce qu'il faut supprimer pendant l'itération. La copie n'est pas non plus possible en raison de contraintes de mémoire.

setOfElementsToRemove() générera un ensemble d'instances SomeClass que je souhaite supprimer, et fillSet(set) remplira l'ensemble d'entrées.

Après avoir recherché Stack Overflow, je n'ai pas pu trouver une bonne solution à ce problème, mais quelques heures plus tard, j'ai réalisé que ce qui suit ferait le travail.

Set<SomeClass> set = new HashSet<SomeClass>();
Set<SomeClass> outputSet = new HashSet<SomeClass>();
fillSet(set);
while (!set.isEmpty()) {
    Iterator<SomeClass> it = set.iterator();
    SomeClass instance = it.next();
    outputSet.add(instance);
    set.removeAll(setOfElementsToRemoveIncludingThePassedValue(instance));
}

setOfElementsToRemoveIncludingThePassedValue() va générer un ensemble d'éléments à supprimer qui inclut la valeur qui lui est transmise. Nous devons supprimer la valeur transmise pour que set se vide.

Ma question est de savoir si quelqu'un a une meilleure façon de procéder ou s'il existe des opérations de collecte qui prennent en charge ce type de suppression.

De plus, j'ai pensé publier ma solution car il semble y avoir un besoin et je voulais contribuer à l'excellente ressource qu'est Stack Overflow.

30
nash

Normalement, lorsque vous supprimez un élément d'une collection tout en bouclant sur la collection, vous obtenez Exception de modification simultanée . C'est en partie pourquoi l'interface Iterator a une méthode remove (). L'utilisation d'un itérateur est le seul moyen sûr de modifier une collection d'éléments tout en les parcourant.

Le code se présenterait comme ceci:

Set<SomeClass> set = new HashSet<SomeClass>();
fillSet(set);
Iterator<SomeClass> setIterator = set.iterator();
while (setIterator.hasNext()) {
    SomeClass currentElement = setIterator.next();
    if (setOfElementsToRemove(currentElement).size() > 0) {
        setIterator.remove();
    }
}

De cette façon, vous supprimerez en toute sécurité tous les éléments qui génèrent un ensemble de suppression de votre setOfElementsToRemove ().

[~ # ~] modifier [~ # ~]

Sur la base d'un commentaire à une autre réponse, cela peut être plus ce que vous voulez:

Set<SomeClass> set = new HashSet<SomeClass>();
Set<SomeClass> removalSet = new HashSet<SomeClass>();
fillSet(set);

for (SomeClass currentElement : set) {
    removalSet.addAll(setOfElementsToRemove(currentElement);
}

set.removeAll(removalSet);
40
Peter

Au lieu de parcourir tous les éléments de l'ensemble pour supprimer ceux que vous voulez, vous pouvez réellement utiliser Google Collections (pas quelque chose que vous ne pouvez pas faire par vous-même cependant) et appliquer un prédicat à mask ceux dont vous n'avez pas besoin.

package com.stackoverflow.q1675037;

import Java.util.HashSet;
import Java.util.Set;

import org.junit.Assert;
import org.junit.Test;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;


public class SetTest
{
public void testFilter(final Set<String> original, final Set<String> toRemove, final Set<String> expected)
{

    Iterable<String> mask = Iterables.filter(original, new Predicate<String>()
    {
        @Override
        public boolean apply(String next) {
        return !toRemove.contains(next);
        }
    });

    HashSet<String> filtered = Sets.newHashSet(mask);

    Assert.assertEquals(original.size() - toRemove.size(), filtered.size());
    Assert.assertEquals(expected, filtered);        
}


@Test
public void testFilterNone()
{
    Set<String> original = new HashSet<String>(){
        {
            this.add("foo");
            this.add("bar");
            this.add("foobar");
        }
    };

    Set<String> toRemove = new HashSet();

    Set<String> expected = new HashSet<String>(){
        {
            this.add("foo");                
            this.add("bar");
            this.add("foobar");
        }
    };

    this.testFilter(original, toRemove, expected);
}

@Test
public void testFilterAll()
{
    Set<String> original = new HashSet<String>(){
        {
            this.add("foo");
            this.add("bar");
            this.add("foobar");
        }
    };

    Set<String> toRemove = new HashSet<String>(){
        {
            this.add("foo");
            this.add("bar");
            this.add("foobar");
        }
    };

    HashSet<String> expected = new HashSet<String>();
    this.testFilter(original, toRemove, expected);
}    

@Test
public void testFilterOne()
{
    Set<String> original = new HashSet<String>(){
        {
            this.add("foo");
            this.add("bar");
            this.add("foobar");
        }
    };

    Set<String> toRemove = new HashSet<String>(){
        {
            this.add("foo");
        }
    };

    Set<String> expected = new HashSet<String>(){
        {
            this.add("bar");
            this.add("foobar");
        }
    };

    this.testFilter(original, toRemove, expected);
}    


@Test
public void testFilterSome()
{
    Set<String> original = new HashSet<String>(){
        {
            this.add("foo");
            this.add("bar");
            this.add("foobar");
        }
    };

   Set<String> toRemove = new HashSet<String>(){
        {
            this.add("bar");
            this.add("foobar");
        }
    };

    Set<String> expected = new HashSet<String>(){
        {
            this.add("foo");
        }
    };

    this.testFilter(original, toRemove, expected);
}    
}
9
qnoid

Vous pouvez essayer le Java.util.concurrent.CopyOnWriteArraySet Qui vous donne un itérateur qui est un instantané de l'ensemble au moment de la création de l'itérateur. Toutes les modifications que vous apportez à l'ensemble (c'est-à-dire en appelant removeAll()) ne seront pas visibles dans l'itérateur, mais sont visibles si vous regardez l'ensemble lui-même (et removeAll() ne le sera pas jeter).

6
joe p

Toute solution qui implique de supprimer de l'ensemble que vous itérez pendant que vous l'itérez, mais pas via l'itérateur, ne fonctionnera absolument pas. Sauf éventuellement: vous pourriez utiliser une Collections.newSetFromMap(new ConcurrentHashMap<SomeClass, Boolean>(sizing params)). Le hic est que maintenant votre itérateur est seulement faiblement cohérent, ce qui signifie que chaque fois que vous supprimez un élément que vous n'avez pas encore rencontré, il n'est pas défini si cet élément apparaîtra plus tard dans votre itération ou non. Si ce n'est pas un problème, cela pourrait fonctionner pour vous.

Une autre chose que vous pouvez faire est de construire un toRemove ensemble au fur et à mesure, puis set.removeAll(itemsToRemove); uniquement à la fin. Vous pouvez également copier l'ensemble avant de commencer, afin de pouvoir itérer une copie tout en la supprimant de l'autre.

EDIT: oups, je vois que Peter Nix avait déjà suggéré l'idée toRemove (bien qu'avec un removeAll inutilement roulé à la main).

6

Il y a une réponse simple à cela - utilisez la méthode Iterator.remove ().

2
Andrzej Doyle

Si vous avez suffisamment de mémoire pour une copie de l'ensemble, je suppose que vous disposez également de suffisamment de mémoire pour deux copies. Les règles Kafka-esque que vous citez ne semblent pas interdire cela :)

Ma suggestion, alors:

fillSet(set);
fillSet(copy);
for (Object item : copy) {
   if (set.contains(item)) { // ignore if not
     set.removeAll(setOfStuffToRemove())
   }
}

donc la copie reste intacte et fournit simplement les éléments à boucler, tandis que l'ensemble subit des suppressions. Les trucs qui ont été retirés du jeu entre-temps seront ignorés.

2
Carl Smotricz

Pourquoi n'utilisez-vous pas la méthode de suppression de l'itérateur sur les objets que vous souhaitez supprimer?

Les itérateurs ont été introduits principalement parce que les énumérateurs ne pouvaient pas gérer la suppression pendant l'énumération.

1
Ben S

Vous devez appeler Iterator.remove méthode.

Notez également que sur la plupart des Java.util collections la méthode remove générera une exception si le contenu de la collection a changé. Donc, si le code est multithread, soyez extrêmement prudent ou utilisez des collections simultanées.

0

Copié à partir de API Java :

L'interface List fournit un itérateur spécial, appelé ListIterator, qui permet l'insertion et le remplacement d'éléments, et l'accès bidirectionnel en plus des opérations normales que l'interface Iterator fournit. Une méthode est fournie pour obtenir un itérateur de liste qui commence à une position spécifiée dans la liste.

Je pensais souligner que le ListIterator, qui est un type spécial d'itérateur, est conçu pour être remplacé.

0
Siri

Il est possible d'implémenter un Set qui permet de supprimer ses éléments tout en itérant dessus.

Je pense que les implémentations standard (HashSet, TreeSet etc.) l'interdisent car cela signifie qu'ils peuvent utiliser des algorithmes plus efficaces, mais ce n'est pas difficile à faire.

Voici un exemple incomplet utilisant Google Collections:

import Java.util.Iterator;
import Java.util.Map;
import Java.util.Set;
import Java.util.concurrent.ConcurrentHashMap;

import com.google.common.base.Predicates;
import com.google.common.collect.ForwardingSet;
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;

public class ConcurrentlyModifiableSet<E>
extends ForwardingSet<E> {
 /** Create a new, empty set */
 public ConcurrentlyModifiableSet() {
  Map<E, Boolean> map = new ConcurrentHashMap<E, Boolean>();
  delegate = Sets.newSetFromMap(map);
 }

 @Override
 public Iterator<E> iterator() {
  return Iterators.filter(delegate.iterator(), Predicates.in(delegate));
 }

 @Override
 protected Set<E> delegate() {
  return this.delegate;
 }

 private Set<E> delegate;
}

Remarque: l'itérateur ne prend pas en charge l'opération remove() (mais l'exemple de la question ne l'exige pas.)

0
finnw