Evitar la ConcurrentModificationException en Java

1. Introducción

En este artículo, echaremos un vistazo a la clase ConcurrentModificationException .

Primero, daremos una explicación de cómo funciona y luego lo demostraremos usando una prueba para activarlo.

Finalmente, probaremos algunas soluciones mediante el uso de ejemplos prácticos.

2. Activación de una ConcurrentModificationException

Esencialmente, la ConcurrentModificationException se usa para fallar rápido cuando se modifica algo en lo que estamos iterando. Probemos esto con una prueba simple:

@Test(expected = ConcurrentModificationException.class) public void whilstRemovingDuringIteration_shouldThrowException() throws InterruptedException { List integers = newArrayList(1, 2, 3); for (Integer integer : integers) { integers.remove(1); } }

Como podemos ver, antes de terminar nuestra iteración estamos eliminando un elemento. Eso es lo que desencadena la excepción.

3. Soluciones

A veces, es posible que deseemos eliminar elementos de una colección mientras iteramos. Si este es el caso, existen algunas soluciones.

3.1. Usar un iterador directamente

Un bucle for-each usa un iterador detrás de escena, pero es menos detallado. Sin embargo, si refactorizamos nuestra prueba anterior para usar un iterador, tendremos acceso a métodos adicionales, como remove (). Intentemos usar este método para modificar nuestra lista en su lugar:

for (Iterator iterator = integers.iterator(); iterator.hasNext();) { Integer integer = iterator.next(); if(integer == 2) { iterator.remove(); } }

Ahora notaremos que no hay excepción. La razón de esto es que el método remove () no causa una ConcurrentModificationException. Es seguro llamar mientras se itera.

3.2. No eliminar durante la iteración

Si queremos mantener nuestro ciclo for-each , entonces podemos. Es solo que debemos esperar hasta después de iterar antes de eliminar los elementos. Probemos esto agregando lo que queremos eliminar a una lista toRemove mientras iteramos :

List integers = newArrayList(1, 2, 3); List toRemove = newArrayList(); for (Integer integer : integers) { if(integer == 2) { toRemove.add(integer); } } integers.removeAll(toRemove); assertThat(integers).containsExactly(1, 3); 

Esta es otra forma eficaz de solucionar el problema.

3.3. Usando removeIf ()

Java 8 introdujo el método removeIf () en la interfaz Collection . Esto significa que si estamos trabajando con él, podemos usar ideas de programación funcional para lograr nuevamente los mismos resultados:

List integers = newArrayList(1, 2, 3); integers.removeIf(i -> i == 2); assertThat(integers).containsExactly(1, 3);

Este estilo declarativo nos ofrece la menor cantidad de verbosidad. Sin embargo, dependiendo del caso de uso, podemos encontrar otros métodos más convenientes.

3.4. Filtrado mediante secuencias

Al sumergirnos en el mundo de la programación funcional / declarativa, podemos olvidarnos de las colecciones mutantes, en cambio, podemos centrarnos en los elementos que realmente deberían procesarse:

Collection integers = newArrayList(1, 2, 3); List collected = integers .stream() .filter(i -> i != 2) .map(Object::toString) .collect(toList()); assertThat(collected).containsExactly("1", "3");

Hemos hecho lo contrario a nuestro ejemplo anterior, proporcionando un predicado para determinar los elementos que se deben incluir, no excluir. La ventaja es que podemos encadenar otras funciones junto con la eliminación. En el ejemplo, usamos un mapa funcional (), pero podríamos usar aún más operaciones si queremos.

4. Conclusión

En este artículo, mostramos los problemas que puede encontrar si elimina elementos de una colección mientras itera, y también brindamos algunas soluciones para negar el problema.

La implementación de estos ejemplos se puede encontrar en GitHub. Este es un proyecto de Maven, por lo que debería ser fácil de ejecutar tal como está.