Iterando a través de una colección, evitando ConcurrentModificationException al eliminar objetos en un bucle


1194

Todos sabemos que no puede hacer lo siguiente por ConcurrentModificationException:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

Pero esto aparentemente funciona a veces, pero no siempre. Aquí hay un código específico:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<>();

    for (int i = 0; i < 10; ++i) {
        l.add(4);
        l.add(5);
        l.add(6);
    }

    for (int i : l) {
        if (i == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

Esto, por supuesto, da como resultado:

Exception in thread "main" java.util.ConcurrentModificationException

A pesar de que múltiples hilos no lo están haciendo. De todas formas.

¿Cuál es la mejor solución para este problema? ¿Cómo puedo eliminar un elemento de la colección en un bucle sin lanzar esta excepción?

También estoy usando un arbitrario Collectionaquí, no necesariamente un ArrayList, por lo que no puede confiar get.


Nota para los lectores: lea docs.oracle.com/javase/tutorial/collections/interfaces/… , puede tener una forma más fácil de lograr lo que desea hacer.
GKFX

Respuestas:


1601

Iterator.remove() es seguro, puedes usarlo así:

List<String> list = new ArrayList<>();

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

Tenga en cuenta que Iterator.remove()es la única forma segura de modificar una colección durante la iteración; el comportamiento no se especifica si la colección subyacente se modifica de alguna otra manera mientras la iteración está en progreso.

Fuente: docs.oracle> La interfaz de la colección


Y de manera similar, si tiene un ListIteratory desea agregar elementos, puede usarListIterator#add , por la misma razón que puede Iterator#remove hacerlo: está diseñado para permitirlo.


En su caso se trató de eliminar de una lista, pero la misma restricción se aplica si intentara puten un Mapmientras que la iteración de su contenido.


19
¿Qué sucede si desea eliminar un elemento que no sea el elemento devuelto en la iteración actual?
Eugen

2
Tienes que usar .remove en el iterador y eso solo puede eliminar el elemento actual, así que no :)
Bill K

1
Tenga en cuenta que esto es más lento en comparación con el uso de ConcurrentLinkedDeque o CopyOnWriteArrayList (al menos en mi caso)
Dan

1
¿No es posible poner la iterator.next()llamada en el ciclo for? Si no, ¿alguien puede explicar por qué?
Blake

1
@GonenI Se implementa para todos los iteradores de colecciones que no son inmutables. List.addes "opcional" en ese mismo sentido también, pero no diría que es "inseguro" agregar a una lista.
Radiodef

345

Esto funciona:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next() == 5) {
        iter.remove();
    }
}

Supuse que, dado que un bucle foreach es azúcar sintáctico para iterar, usar un iterador no ayudaría ... pero le brinda esta .remove()funcionalidad.


43
foreach loop es azúcar sintáctico para iterar. Sin embargo, como señaló, debe llamar a remove en el iterador, a lo que foreach no le da acceso. De ahí la razón por la que no puede eliminar en un bucle foreach (a pesar de que realmente está utilizando un iterador debajo del capó)
madlep

36
+1, por ejemplo, código para usar iter.remove () en contexto, que la respuesta de Bill K no tiene [directamente].
Edificado el

202

Con Java 8 puede usar el nuevo removeIfmétodo . Aplicado a tu ejemplo:

Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);

3
Ooooo! Esperaba que algo en Java 8 o 9 pudiera ayudar. Esto todavía me parece bastante detallado, pero todavía me gusta.
James T Snell

¿Se recomienda implementar equals () también en este caso?
Anmol Gupta

por cierto removeIfutiliza Iteratory whilebucle. Puedes verlo en java 8java.util.Collection.java
omerhakanbilici

3
@omerhakanbilici Algunas implementaciones como ArrayListanularlo por razones de rendimiento. Al que se refiere es solo la implementación predeterminada.
Didier L

@AnmolGupta: No, aquí equalsno se usa en absoluto, por lo que no tiene que implementarse. (Pero, por supuesto, si lo usa equalsen su prueba, entonces debe implementarse de la manera que lo desee.)
Lii

42

Dado que la pregunta ya ha sido respondida, es decir, la mejor manera es usar el método remove del objeto iterador, entraría en los detalles del lugar donde "java.util.ConcurrentModificationException"se arroja el error .

Cada clase de colección tiene una clase privada que implementa la interfaz Iterator y proporciona métodos como next(), remove()y hasNext().

El código para el siguiente se parece a esto ...

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

Aquí el método checkForComodificationse implementa como

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Entonces, como puede ver, si intenta explícitamente eliminar un elemento de la colección. Resulta modCountdiferente de expectedModCount, lo que resulta en la excepción ConcurrentModificationException.


Muy interesante. ¡Gracias! A menudo no llamo a remove (), prefiero borrar la colección después de recorrerla. No quiere decir que sea un buen patrón, justo lo que he estado haciendo últimamente.
James T Snell

26

Puede usar el iterador directamente como lo mencionó, o bien mantener una segunda colección y agregar cada elemento que desea eliminar a la nueva colección, luego eliminar todo al final. Esto le permite seguir usando la seguridad de tipo del bucle para cada uno a costa de un mayor uso de memoria y tiempo de CPU (no debería ser un gran problema a menos que tenga listas realmente grandes o una computadora realmente vieja)

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<>();
    for (int i=0; i < 10; i++) {
        l.add(Integer.of(4));
        l.add(Integer.of(5));
        l.add(Integer.of(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5) {
            itemsToRemove.add(i);
        }
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

77
Esto es lo que normalmente hago, pero el iterador explícito es una solución más elegante que siento.
Claudiu

1
Es justo, siempre y cuando no esté haciendo nada más con el iterador: tenerlo expuesto hace que sea más fácil hacer cosas como llamar a .next () dos veces por ciclo, etc. No es un gran problema, pero puede causar problemas si lo está haciendo. algo más complicado que simplemente recorrer una lista para eliminar entradas.
RodeoClown

@RodeoClown: en la pregunta original, Claudiu está eliminando de la Colección, no del iterador.
mate b

1
Eliminar del iterador elimina de la colección subyacente ... pero lo que estaba diciendo en el último comentario es que si está haciendo algo más complicado que solo buscar eliminaciones en el ciclo (como procesar datos correctos) usando el iterador puede hacer algo errores más fáciles de hacer.
RodeoClown

Si se trata de una simple eliminación de valores que no son necesarios y el ciclo solo está haciendo esa única cosa, usar el iterador directamente y llamar a .remove () está absolutamente bien.
RodeoClown

17

En tales casos, un truco común es (¿era?) Ir hacia atrás:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

Dicho esto, estoy más que feliz de que tenga mejores formas en Java 8, por ejemplo, removeIfo filteren las transmisiones.


2
Este es un buen truco. Pero no funcionaría en colecciones no indexadas como conjuntos, y sería muy lento en las listas vinculadas.
Claudiu

@Claudiu Sí, esto es definitivamente solo para ArrayListcolecciones similares.
Landei

Estoy usando una ArrayList, esto funcionó perfectamente, gracias.
StarSweeper

2
Los índices son geniales. Si es tan común, ¿por qué no lo usas for(int i = l.size(); i-->0;) {?
John

16

Misma respuesta que Claudio con un bucle for:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

12

Con Eclipse Collections , el método removeIfdefinido en MutableCollection funcionará:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

Con la sintaxis Lambda de Java 8, esto se puede escribir de la siguiente manera:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

La llamada a Predicates.cast()es necesaria aquí porque removeIfse agregó un método predeterminado en la java.util.Collectioninterfaz en Java 8.

Nota: Soy un committer para Eclipse Collections .


10

Haga una copia de la lista existente e itere sobre una nueva copia.

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

19
Hacer una copia suena como un desperdicio de recursos.
Antzi

3
@Antzi Eso depende del tamaño de la lista y la densidad de los objetos dentro. Sigue siendo una solución valiosa y válida.
MRE

He estado usando este método. Se necesita un poco más de recursos, pero mucho más flexible y claro.
Tao Zhang

Esta es una buena solución cuando no tiene la intención de eliminar objetos dentro del ciclo en sí, sino que se eliminan "al azar" de otros hilos (por ejemplo, operaciones de red que actualizan datos). Si te encuentras haciendo estas copias mucho, incluso hay una implementación de Java que hace exactamente esto: docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
A1m

8

Las personas afirman que no se puede eliminar de una Colección que se repite por un bucle foreach. Solo quería señalar que es técnicamente incorrecto y describir exactamente (sé que la pregunta del OP es tan avanzada como para evitar saber esto) el código detrás de esa suposición:

for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
    if (obj.isTouched()) {
        untouchedSet.remove(obj);
        touchedSt.add(obj);
        break;  // this is key to avoiding returning to the foreach
    }
}

No es que no pueda eliminar de la Colletioniteración, sino que no puede continuar la iteración una vez que lo hace. Por lo tanto, labreak en el código anterior.

Disculpas si esta respuesta es un caso de uso algo especializado y más adecuado para el hilo original del que llegué aquí, ese está marcado como duplicado (a pesar de que este hilo parece más matizado) de esto y bloqueado.


8

Con un bucle tradicional

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

Ah, así que en realidad es solo el bucle mejorado para lanzar la excepción.
cellepo

FWIW: el mismo código seguiría funcionando después de modificarlo para incrementarlo i++en la protección del bucle en lugar de hacerlo dentro del cuerpo del bucle.
cellepo

Corrección ^: Eso es si el i++incremento no fuera condicional - Ahora veo que es por eso que lo haces en el cuerpo :)
cellepo

2

A le ListIteratorpermite agregar o eliminar elementos en la lista. Supongamos que tiene una lista de Carobjetos:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

Los métodos adicionales en la interfaz ListIterator (extensión de Iterator) son interesantes, particularmente su previousmétodo.
cellepo

1

Tengo una sugerencia para el problema anterior. No necesita lista secundaria ni tiempo extra. Encuentre un ejemplo que haga lo mismo pero de manera diferente.

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

Esto evitaría la excepción de concurrencia.


1
La pregunta establece explícitamente que no es necesario utilizar el OP ArrayListy, por lo tanto, no puede confiar en él get(). Sin embargo, probablemente sea un buen enfoque.
kaskelotti

(Aclaración ^) OP está utilizando una interfaz arbitraria Collection- Collectionno incluye get. (Aunque la Listinterfaz FWIW incluye 'get').
cellepo

Acabo de agregar una respuesta separada más detallada aquí también para while-looping a List. Pero +1 para esta respuesta porque vino primero.
cellepo

1

ConcurrentHashMap o ConcurrentLinkedQueue o ConcurrentSkipListMap puede ser otra opción, porque nunca arrojarán ninguna ConcurrentModificationException, incluso si elimina o agrega un elemento.


Sí, y tenga en cuenta que todos están en el java.util.concurrentpaquete. Algunas otras clases de casos de uso común / similares de ese paquete son CopyOnWriteArrayList & CopyOnWriteArraySet [pero no se limitan a esas].
cellepo

En realidad, acabo de enterarme de que, aunque esos objetos de estructura de datos evitan ConcurrentModificationException , usarlos en un bucle mejorado todavía puede causar problemas de indexación (es decir: omitir elementos o IndexOutOfBoundsException...)
cellepo

1

Sé que esta pregunta es demasiado antigua para ser sobre Java 8, pero para aquellos que usan Java 8, pueden usar removeIf () fácilmente:

Collection<Integer> l = new ArrayList<Integer>();

for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
}

l.removeIf(i -> i.intValue() == 5);

1

Otra forma es crear una copia de su arrayList:

List<Object> l = ...

List<Object> iterationList = ImmutableList.copyOf(l);

for (Object i : iterationList) {
    if (condition(i)) {
        l.remove(i);
    }
}

Nota: ino es un indexsino el objeto. Tal vez llamarlo objsería más apropiado.
luckydonald

1

La mejor manera (recomendada) es el uso del paquete java.util.Concurrent. Al usar este paquete, puede evitar fácilmente esta excepción. referir código modificado

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<Integer>();

    for (int i=0; i < 10; ++i) {
        l.add(new Integer(4));
        l.add(new Integer(5));
        l.add(new Integer(6));
    }

    for (Integer i : l) {
        if (i.intValue() == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

0

En el caso de ArrayList: remove (int index) - if (index es la posición del último elemento) evita sin System.arraycopy()y no toma tiempo para esto.

el tiempo de la matriz aumenta si (el índice disminuye), por cierto, los elementos de la lista también disminuyen.

la mejor manera efectiva de eliminar es eliminar sus elementos en orden descendente: while(list.size()>0)list.remove(list.size()-1);// toma O (1) while(list.size()>0)list.remove(0);// toma O (factorial (n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion
  • para el índice de bucle: 1090 ms
  • para el índice desc: 519 mseg --- el mejor
  • para iterador: 1043 ms

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

El truco es después de eliminar el elemento de la lista si omite la llamada interna iterator.next (). ¡aún funciona! Aunque no propongo escribir código como este, ayuda a entender el concepto detrás de él :-)

¡Salud!


0

Ejemplo de modificación de colección segura para subprocesos:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

0

Sé que esta pregunta supone solo una Collection, y no más específicamente ninguna List. Pero para aquellos que leen esta pregunta que realmente están trabajando con una Listreferencia, puede evitar ConcurrentModificationExceptioncon un whilebucle (mientras modifica dentro de ella) si quiere evitarloIterator (ya sea si desea evitarlo en general o evitarlo específicamente para lograr un orden de bucle diferente de principio a fin que se detiene en cada elemento [que creo que es el único orden que Iteratorpuede hacer]):

* Actualización: vea los comentarios a continuación que aclaran que lo análogo también se puede lograr con el tradicional para bucle.

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

Sin ConcurrentModificationException de ese código.

Allí vemos que el bucle no comienza desde el principio y no se detiene en cada elemento (lo que creo Iteratorque no puede hacer).

FWIW también vemos que getse llama list, lo que no se podría hacer si su referencia fuera solo Collection(en lugar del Listtipo más específico de Collection): la Listinterfaz incluye get, pero la Collectioninterfaz no. Si no fuera por esa diferencia, entonces la listreferencia podría ser una Collection[y, por lo tanto, técnicamente esta respuesta sería una respuesta directa, en lugar de una respuesta tangencial].

El mismo código FWIWW sigue funcionando después de la modificación para comenzar al principio en cada elemento (al igual que el Iteratororden):

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

Sin embargo, esto todavía requiere un cálculo muy cuidadoso de las indicaciones para eliminar.
OneCricketeer

Además, esta es solo una explicación más detallada de esta respuesta stackoverflow.com/a/43441822/2308683
OneCricketeer

Es bueno saberlo, ¡gracias! Esa otra respuesta me ayudó a entender que es el bucle mejorado para el bucle lo que arrojaría ConcurrentModificationException, pero no el bucle tradicional para el bucle (que usa la otra respuesta), sin darme cuenta de que antes es por eso que estaba motivado para escribir esta respuesta (erróneamente pensó entonces que era todos los bucles para-que emitir la excepción).
cellepo

0

Una solución podría ser rotar la lista y eliminar el primer elemento para evitar la ConcurrentModificationException o IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

0

Pruebe este (elimina todos los elementos de la lista que sean iguales i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

0

también puedes usar Recursion

La recursión en Java es un proceso en el cual un método se llama a sí mismo continuamente. Un método en Java que se llama a sí mismo se llama método recursivo.


-2

esta podría no ser la mejor manera, pero para la mayoría de los casos pequeños esto debería ser aceptable:

"crea una segunda matriz vacía y agrega solo las que quieras conservar"

No recuerdo de dónde leí esto ... para ser justos, haré este wiki con la esperanza de que alguien lo encuentre o simplemente para no ganar reputación que no merezco.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.