La colección arroja o no lanza ConcurrentModificationException en función del contenido de la Colección

El siguiente código Java arroja una ConcurrentModificationException , como se esperaba:

 public class Evil { public static void main(String[] args) { Collection c = new ArrayList(); c.add("lalala"); c.add("sososo"); c.add("ahaaha"); removeLalala(c); System.err.println(c); } private static void removeLalala(Collection c) { for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { c.remove(s); } } } } 

Pero el siguiente ejemplo, que difiere solo en los contenidos de la Collection , se ejecuta sin excepción:

 public class Evil { public static void main(String[] args) { Collection c = new ArrayList(); c.add("lalala"); c.add("lalala"); removeLalala(c); System.err.println(c); } private static void removeLalala(Collection c) { for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { c.remove(s); } } } } 

Esto imprime la salida “[lalala]”. ¿Por qué el segundo ejemplo no arroja una ConcurrentModificationException cuando lo hace el primer ejemplo?

Respuesta corta

Porque el comportamiento a prueba de fallos de un iterador no está garantizado.

Respuesta larga

Obtendrá esta excepción porque no puede manipular una colección mientras itera sobre ella, excepto a través del iterador.

Malo:

 // we're using iterator for (Iterator i = c.iterator(); i.hasNext();) { // here, the collection will check it hasn't been modified (in effort to fail fast) String s = i.next(); if(s.equals("lalala")) { // s is removed from the collection and the collection will take note it was modified c.remove(s); } } 

Bueno:

 // we're using iterator for (Iterator i = c.iterator(); i.hasNext();) { // here, the collection will check it hasn't been modified (in effort to fail fast) String s = i.next(); if(s.equals("lalala")) { // s is removed from the collection through iterator, so the iterator knows the collection changed and can resume the iteration i.remove(); } } 

Ahora al “por qué”: en el código anterior, observe cómo se realiza la verificación de modificación: la eliminación marca la colección como modificada, y la siguiente iteración comprueba si hay alguna modificación y falla si detecta que la colección ha cambiado. Otra cosa importante es que ArrayList (no estoy seguro acerca de otras colecciones) no verifica la modificación en hasNext() .

Por lo tanto, pueden suceder dos cosas extrañas:

  • Si elimina el último elemento mientras itera, no se lanzará nada
    • Eso es porque no hay un elemento “siguiente”, por lo que la iteración finaliza antes de llegar al código de comprobación de modificaciones
  • Si elimina el penúltimo elemento, ArrayList.hasNext() también devolverá false , porque el current index del iterador apunta ahora al último elemento (anterior penúltimo).
    • Entonces, incluso en este caso, no hay un elemento “siguiente” después de la eliminación

Tenga en cuenta que todo esto está en línea con la documentación de ArrayList :

Tenga en cuenta que el comportamiento a prueba de fallos de un iterador no se puede garantizar, ya que, en términos generales, es imposible hacer ninguna garantía dura en presencia de una modificación concurrente no sincronizada. Los iteradores a prueba de errores lanzan ConcurrentModificationException sobre la base del mejor esfuerzo. Por lo tanto, sería incorrecto escribir un progtwig que dependiera de esta excepción para su corrección: el comportamiento a prueba de fallas de los iteradores debería usarse solo para detectar errores.

Editado para agregar:

Esta pregunta proporciona cierta información sobre por qué la comprobación de modificación simultánea no se realiza en hasNext() y solo se realiza en next() .

Si observa el código fuente del iterador ArrayList (clase anidada privada Itr ), verá el error en el código.

Se supone que el código es fail-fast, que se realiza internamente en el iterador llamando a checkForComodification() , sin embargo, el hasNext() no hace esa llamada, probablemente por razones de rendimiento.

El hasNext() cambio es simplemente:

 public boolean hasNext() { return cursor != size; } 

Esto significa que cuando está en el segundo elemento de la lista y luego elimina un elemento (cualquier elemento), el tamaño se reduce y hasNext() cree que está en el último elemento (que no era), y devuelve false , omitiendo la iteración del último elemento sin error.

OOPS !!!!

De otras respuestas, usted sabe cuál es la forma correcta de eliminar un elemento en la colección mientras itera la colección. Doy aquí la explicación de la pregunta básica. Y la respuesta a su pregunta se encuentra en el siguiente rastro de stack

 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(Unknown Source) at java.util.ArrayList$Itr.next(Unknown Source) at com.ii4sm.controller.Evil.removeLalala(Evil.java:23) at com.ii4sm.controller.Evil.main(Evil.java:17) 

En stacktrace es obvio que i.next(); línea arroja el error. Pero cuando solo tienes dos elementos en la colección.

 Collection c = new ArrayList(); c.add("lalala"); c.add("lalala"); removeLalala(c); System.err.println(c); 

Cuando se elimina el primero, i.hasNext() devuelve falso y i.next() nunca se ejecuta para lanzar la excepción

debe eliminar directamente del iterator (i), no de la collection (c);

prueba esto:

 for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { i.remove(); //note here, changing c to i with no parameter. } } 

EDITAR:

La razón por la cual el primer bash arroja una excepción mientras que el segundo no lo hace es simplemente por la cantidad de elementos en su colección.

ya que el primero pasará por el ciclo más de una vez y el segundo solo repetirá una vez. por lo tanto, no tiene la posibilidad de lanzar excepción

No puede eliminar de la lista si lo está navegando con el bucle “for each”.

No puede eliminar un elemento de una colección sobre la que está iterando. Puede evitar esto utilizando explícitamente un iterador y eliminando el elemento allí. Puedes usar Iterator.

Si usa el siguiente código, no obtendrá ninguna excepción:

 private static void removeLalala(Collection c) { /*for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { c.remove(s); } }*/ Iterator it = c.iterator(); while (it.hasNext()) { String st = it.next(); if (st.equals("lalala")) { it.remove(); } } }