Eliminar elementos de una colección en Java mientras se itera sobre ella

Quiero poder eliminar varios elementos de un conjunto mientras estoy iterando sobre él. Inicialmente esperaba que los iteradores fueran lo suficientemente inteligentes como para que la siguiente solución ingenua funcione.

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

Pero esto arroja una ConcurrentModificationException .

Tenga en cuenta que iterator.remove () no funcionará tanto como yo pueda ver porque necesito eliminar varias cosas a la vez. Supongamos también que no es posible identificar qué elementos eliminar “sobre la marcha”, pero es posible escribir el método setOfElementsToRemove() . En mi caso específico, tomaría mucha memoria y tiempo de procesamiento para determinar qué eliminar mientras se iteraba. Hacer copias tampoco es posible debido a restricciones de memoria.

setOfElementsToRemove() generará un conjunto de instancias SomeClass que deseo eliminar, y fillSet(set) llenará el conjunto con entradas.

Después de buscar Stack Overflow no pude encontrar una buena solución a este problema, pero unas pocas horas después me di cuenta de que lo siguiente haría el trabajo.

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

setOfElementsToRemoveIncludingThePassedValue() generará un conjunto de elementos para eliminar que incluye el valor que se le pasa. Necesitamos eliminar el valor pasado para que el set se vacíe.

Mi pregunta es si alguien tiene una mejor manera de hacerlo o si hay operaciones de recolección que admitan este tipo de eliminaciones.

Además, pensé que publicaría mi solución porque parece que hay una necesidad y quería contribuir con el excelente recurso que es Stack Overflow.

Normalmente, cuando elimina un elemento de una colección al pasar por encima de la colección, obtendrá una Excepción de Modificación Concurrente . Esto es parcialmente por qué la interfaz del iterador tiene un método remove (). El uso de un iterador es la única forma segura de modificar una colección de elementos al atravesarlos.

El código sería algo como esto:

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

De esta forma, eliminará de forma segura todos los elementos que generan un conjunto de eliminación de su setOfElementsToRemove ().

EDITAR

Basado en un comentario a otra respuesta, esto puede ser más de lo que desea:

 Set set = new HashSet(); Set removalSet = new HashSet(); fillSet(set); for (SomeClass currentElement : set) { removalSet.addAll(setOfElementsToRemove(currentElement); } set.removeAll(removalSet); 

En lugar de iterar a través de todos los elementos en el Conjunto para eliminar los que desea, puede usar Google Collections (no es algo que no puede hacer por su cuenta) y aplicar un Predicado para enmascarar los que no necesita .

 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 original, final Set toRemove, final Set expected) { Iterable mask = Iterables.filter(original, new Predicate() { @Override public boolean apply(String next) { return !toRemove.contains(next); } }); HashSet filtered = Sets.newHashSet(mask); Assert.assertEquals(original.size() - toRemove.size(), filtered.size()); Assert.assertEquals(expected, filtered); } @Test public void testFilterNone() { Set original = new HashSet(){ { this.add("foo"); this.add("bar"); this.add("foobar"); } }; Set toRemove = new HashSet(); Set expected = new HashSet(){ { this.add("foo"); this.add("bar"); this.add("foobar"); } }; this.testFilter(original, toRemove, expected); } @Test public void testFilterAll() { Set original = new HashSet(){ { this.add("foo"); this.add("bar"); this.add("foobar"); } }; Set toRemove = new HashSet(){ { this.add("foo"); this.add("bar"); this.add("foobar"); } }; HashSet expected = new HashSet(); this.testFilter(original, toRemove, expected); } @Test public void testFilterOne() { Set original = new HashSet(){ { this.add("foo"); this.add("bar"); this.add("foobar"); } }; Set toRemove = new HashSet(){ { this.add("foo"); } }; Set expected = new HashSet(){ { this.add("bar"); this.add("foobar"); } }; this.testFilter(original, toRemove, expected); } @Test public void testFilterSome() { Set original = new HashSet(){ { this.add("foo"); this.add("bar"); this.add("foobar"); } }; Set toRemove = new HashSet(){ { this.add("bar"); this.add("foobar"); } }; Set expected = new HashSet(){ { this.add("foo"); } }; this.testFilter(original, toRemove, expected); } } 

Cualquier solución que implique eliminar del conjunto que está iterando mientras la itera, pero no a través del iterador, no funcionará. Excepto posiblemente uno: podría usar Collections.newSetFromMap(new ConcurrentHashMap( sizing params )) . El inconveniente es que ahora su iterador solo es débilmente consistente , lo que significa que cada vez que elimina un elemento que aún no ha encontrado, no está definido si ese elemento aparecerá más tarde en su iteración o no. Si eso no es un problema, esto podría funcionar para usted.

Otra cosa que puede hacer es crear un conjunto para toRemove en su lugar, luego set.removeAll(itemsToRemove); solo al final. O bien, copie el conjunto antes de comenzar, de modo que pueda repetir una copia mientras quita la otra.

EDITAR: oops, veo que Peter Nix ya había sugerido la idea de toRemove (aunque con un removeAll innecesariamente enrollado a removeAll ).

Puede probar java.util.concurrent.CopyOnWriteArraySet que le proporciona un iterador que es una instantánea del conjunto en el momento de la creación del iterador. Cualquier cambio que realice en el conjunto (es decir, al llamar a removeAll() ) no será visible en el iterador, pero estará visible si mira el conjunto en sí (y removeAll() no arrojará).

Hay una respuesta simple a esto: use el método Iterator.remove ().