Java Language Eliminar elementos de una lista dentro de un bucle


Ejemplo

Es difícil eliminar elementos de una lista mientras se encuentra dentro de un bucle, esto se debe al hecho de que el índice y la longitud de la lista se modifican.

Dada la siguiente lista, aquí hay algunos ejemplos que darán un resultado inesperado y otros que darán el resultado correcto.

List<String> fruits = new ArrayList<String>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Strawberry");

INCORRECTO

Extracción de iteración de for declaración Omite "Banana":

El ejemplo de código solo imprimirá Apple y Strawberry . Banana se omite porque se mueve al índice 0 vez Apple se elimina, pero, al mismo tiempo i se incrementa a 1 .

for (int i = 0; i < fruits.size(); i++) {
    System.out.println (fruits.get(i)); 
    if ("Apple".equals(fruits.get(i))) {
         fruits.remove(i);
    }     
}

Eliminando en la excepción mejorada for tiros de instrucción :

Debido a iterar sobre la colección y modificarla al mismo tiempo.

Emite: java.util.ConcurrentModificationException

for (String fruit : fruits) { 
    System.out.println(fruit);
    if ("Apple".equals(fruit)) {
        fruits.remove(fruit);
    }
}

CORRECTO

Eliminando en bucle while usando un Iterator

Iterator<String> fruitIterator = fruits.iterator();
while(fruitIterator.hasNext()) {     
    String fruit = fruitIterator.next();     
    System.out.println(fruit);
    if ("Apple".equals(fruit)) {
        fruitIterator.remove();
    } 
}

La interfaz Iterator tiene un método remove() incorporado solo para este caso. Sin embargo, este método está marcado como "opcional" en la documentación y podría generar una UnsupportedOperationException .

Emite: UnsupportedOperationException - si la operación de eliminación no es compatible con este iterador

Por lo tanto, es recomendable consultar la documentación para asegurarse de que esta operación sea compatible (en la práctica, a menos que la recopilación sea una inmutable obtenida a través de una biblioteca de terceros o el uso de uno de los métodos Collections.unmodifiable...() , la operación es casi siempre soportada).


Al utilizar un Iterator una Iterator ConcurrentModificationException cuando se modCount el modCount de la List desde que se creó el Iterator . Esto podría haber ocurrido en el mismo hilo o en una aplicación multihilo compartiendo la misma lista.

Un modCount es una variable int que cuenta el número de veces que esta lista ha sido modificada estructuralmente. Un cambio estructural significa esencialmente una operación add() o remove() que se invoca en el objeto Collection (los cambios realizados por Iterator no se cuentan). Cuando se crea el Iterator , almacena este modCount y en cada iteración de la List comprueba si el modCount actual es el mismo que cuando se creó el Iterator . Si hay un cambio en el valor de modCount , lanza una ConcurrentModificationException .

Por lo tanto, para la lista declarada anteriormente, una operación como la siguiente no arrojará ninguna excepción:

Iterator<String> fruitIterator = fruits.iterator();
fruits.set(0, "Watermelon");
while(fruitIterator.hasNext()){
    System.out.println(fruitIterator.next());
}

Pero agregar un nuevo elemento a la List después de inicializar un Iterator lanzará una ConcurrentModificationException :

Iterator<String> fruitIterator = fruits.iterator();
fruits.add("Watermelon");
while(fruitIterator.hasNext()){
    System.out.println(fruitIterator.next());    //ConcurrentModificationException here
}

Iterando hacia atrás

for (int i = (fruits.size() - 1); i >=0; i--) {
    System.out.println (fruits.get(i));
    if ("Apple".equals(fruits.get(i))) {
         fruits.remove(i);
    }
}

Esto no se salta nada. La desventaja de este enfoque es que la salida es inversa. Sin embargo, en la mayoría de los casos, elimina elementos que no importan. Nunca debes hacer esto con LinkedList .

Iterando hacia adelante, ajustando el índice de bucle.

for (int i = 0; i < fruits.size(); i++) {
    System.out.println (fruits.get(i)); 
    if ("Apple".equals(fruits.get(i))) {
         fruits.remove(i);
         i--;
    }     
}

Esto no se salta nada. Cuando el elemento i th se elimina de la List , el elemento originalmente posicionado en el índice i+1 convierte en el nuevo elemento i th. Por lo tanto, el bucle puede decrementar i para que la siguiente iteración procese el siguiente elemento, sin saltarse.

Usando una lista de "debería ser eliminado"

ArrayList shouldBeRemoved = new ArrayList();
for (String str : currentArrayList) {
    if (condition) {
        shouldBeRemoved.add(str);
    }
}
currentArrayList.removeAll(shouldBeRemoved);

Esta solución permite al desarrollador verificar si los elementos correctos se eliminan de forma más limpia.

Java SE 8

En Java 8 son posibles las siguientes alternativas. Estos son más limpios y más directos si la eliminación no tiene que suceder en un bucle.

Filtrando una corriente

Una List puede ser transmitida y filtrada. Se puede utilizar un filtro adecuado para eliminar todos los elementos no deseados.

List<String> filteredList = 
    fruits.stream().filter(p -> !"Apple".equals(p)).collect(Collectors.toList());

Tenga en cuenta que a diferencia de todos los otros ejemplos aquí, este ejemplo produce una nueva instancia de List y mantiene la List original sin cambios.

Utilizando removeIf

Ahorra la sobrecarga de construir un flujo si todo lo que se necesita es eliminar un conjunto de elementos.

fruits.removeIf(p -> "Apple".equals(p));