Suppression d'éléments d'une collection en java tout en itérant dessus
Je veux être capable de supprimer plusieurs éléments d'un ensemble pendant que je l'itère. Au départ, j'espérais que les itérateurs étaient assez 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 jette un ConcurrentModificationException
.
Notez cet itérateur.remove() ne fonctionnera pas pour autant que je peux voir parce que j'ai besoin de supprimer plusieurs choses à la fois. Supposons également qu'il n'est pas possible d'identifier les éléments à supprimer "à la volée", mais il est possible d'écrire la méthode setOfElementsToRemove()
. Dans mon cas spécifique, il serait prendre beaucoup de mémoire et de temps de traitement pour déterminer ce qu'il faut supprimer lors de l'itération. Faire des copies n'est pas non plus possible en raison de contraintes de mémoire.
setOfElementsToRemove()
va générer un ensemble D'instances SomeClass que je veux supprimer, et fillSet(set)
remplira l'ensemble avec des entrées.
Après avoir cherché 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érez un ensemble d'éléments à supprimer qui inclut la valeur qui lui est transmise. Nous devons supprimer la valeur passée afin que set
se vide.
Ma question Est de savoir si quelqu'un a une meilleure façon de le faire ou s'il existe des opérations de collecte qui prennent en charge ce genre de suppressions.
Aussi, je pensais que je publierais ma solution car il semble y avoir un besoin et je voulais contribuer à l'excellente ressource qui est Stack Overflow.
10 réponses
Normalement, lorsque vous supprimez un élément d'une collection tout en faisant une boucle sur la collection, vous obtenez une 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 traversant.
Le code irait quelque chose 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 setOfElementsToRemove ().
Modifier
Basé sur 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);
Au lieu de parcourir tous les éléments de l'ensemble pour supprimer ceux que vous voulez, vous pouvez utiliser Google Collections (ce n'est pas quelque chose que vous ne pouvez pas faire vous-même) 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);
}
}
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 peut-être un: vous pouvez utiliser un Collections.newSetFromMap(new ConcurrentHashMap<SomeClass, Boolean>(sizing params))
. Le hic est que maintenant votre itérateur n'est que 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.
Un autre la chose que vous pouvez faire est de construire un ensemble toRemove
au fur et à mesure, puis set.removeAll(itemsToRemove);
seulement à la fin. Ou, copiez l'ensemble avant de commencer, de sorte que vous pouvez itérer une copie tout en 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).
, Vous pouvez essayer les java.util.concurrent.CopyOnWriteArraySet
, qui vous donne un itérateur qui est un instantané de l'ensemble au moment de l'itérateur de la création. 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 lancera pas).
Il y a une réponse simple à cela-utilisez L'itérateur.méthode remove ().
Si vous avez assez de mémoire pour une copie de l'ensemble, je suppose que vous avez aussi assez de mémoire pour deux copies. Les règles de 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 choses à Boucler, tandis que set souffre de suppressions. Les choses qui ont été supprimées de l'ensemble entre-temps seront ignorées.
Pourquoi n'utilisez-vous pas la itérateur de supprimer la méthode 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.
Vous devriez appeler Iterator.remove
méthode.
Notez également que sur la plupart des collections java.util
, la méthode remove
générera une exception si le contenu de la collection a changé. Donc, si le code est multi-thread, utilisez une prudence supplémentaire ou utilisez des collections simultanées.
Il est possible d'implémenter un {[1] } qui permet de supprimer ses éléments tout en itérant dessus.
Je pense que les implémentations standard (HashSet, TreeSet etc.) l'interdire parce que 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;
}
Note: l'itérateur ne prend pas en charge l'opération remove()
(mais l'exemple de la question ne l'exige pas.)
Copié à partir de l'API Java :
L'interface de liste 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'itérateur l'interface fournit. Une méthode pour obtenir une liste itérateur qui commence à une position spécifiée dans la liste.
Je pensais que je soulignerais que le ListIterator qui est un type spécial D'itérateur est construit pour le remplacement.