Eliminar elementos de la colección mientras se itera

Resuelto user1329572 asked hace 12 años • 9 respuestas

AFAIK, hay dos enfoques:

  1. Iterar sobre una copia de la colección.
  2. Utilice el iterador de la colección real.

Por ejemplo,

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy){
    // modify actual fooList
}

y

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext()){
    // modify actual fooList using itr.remove()
}

¿Hay alguna razón para preferir un enfoque sobre el otro (por ejemplo, preferir el primer enfoque por la simple razón de legibilidad)?

user1329572 avatar May 03 '12 20:05 user1329572
Aceptado

Permítanme darles algunos ejemplos con algunas alternativas para evitar un archivo ConcurrentModificationException.

Supongamos que tenemos la siguiente colección de libros.

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

Recoger y eliminar

La primera técnica consiste en recopilar todos los objetos que queremos eliminar (por ejemplo, usando un bucle for mejorado) y, una vez que terminamos de iterar, eliminamos todos los objetos encontrados.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
    if(book.getIsbn().equals(isbn)){
        found.add(book);
    }
}
books.removeAll(found);

Esto supone que la operación que desea realizar es "eliminar".

Si desea "agregar", este enfoque también funcionaría, pero supongo que iteraría sobre una colección diferente para determinar qué elementos desea agregar a una segunda colección y luego emitiría un addAllmétodo al final.

Usando ListIterator

Si está trabajando con listas, otra técnica consiste en utilizar una ListIteratorque tenga soporte para eliminar y agregar elementos durante la iteración misma.

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}

Nuevamente, utilicé el método "eliminar" en el ejemplo anterior, que es lo que parecía implicar su pregunta, pero también puede usar su addmétodo para agregar nuevos elementos durante la iteración.

Usando JDK >= 8

Para aquellos que trabajan con Java 8 o versiones superiores, existen un par de técnicas más que pueden utilizar para aprovecharlo.

Podrías usar el nuevo removeIfmétodo en la Collectionclase base:

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

O utilice la nueva API de transmisión:

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
                           .filter(b -> b.getIsbn().equals(other))
                           .collect(Collectors.toList());

En este último caso, para filtrar elementos de una colección, reasigna la referencia original a la colección filtrada (es decir books = filtered), o utiliza la colección filtrada a removeAlllos elementos encontrados de la colección original (es decir books.removeAll(filtered)).

Usar sublista o subconjunto

También existen otras alternativas. Si la lista está ordenada y desea eliminar elementos consecutivos, puede crear una sublista y luego borrarla:

books.subList(0,5).clear();

Dado que la sublista está respaldada por la lista original, esta sería una forma eficaz de eliminar esta subcolección de elementos.

Se podría lograr algo similar con NavigableSet.subSetel método de uso de conjuntos ordenados o cualquiera de los métodos de corte que se ofrecen allí.

Consideraciones:

El método que utilice puede depender de lo que pretenda hacer.

  • La recopilación y removeAlla técnica funcionan con cualquier Colección (Colección, Lista, Conjunto, etc.).
  • Obviamente, la ListIteratortécnica solo funciona con listas, siempre que su ListIteratorimplementación dada ofrezca soporte para operaciones de agregar y eliminar.
  • El Iteratorenfoque funcionaría con cualquier tipo de colección, pero solo admite operaciones de eliminación.
  • Con el enfoque ListIterator/ Iteratorla ventaja obvia es no tener que copiar nada ya que lo eliminamos a medida que iteramos. Entonces esto es muy eficiente.
  • El ejemplo de secuencias de JDK 8 en realidad no eliminó nada, pero buscó los elementos deseados y luego reemplazamos la referencia de colección original con la nueva y dejamos que la antigua se recolecte como basura. Entonces, iteramos solo una vez sobre la colección y eso sería eficiente.
  • En el método de recopilación y removeAllenfoque, la desventaja es que tenemos que iterar dos veces. Primero iteramos en el foor-loop buscando un objeto que coincida con nuestros criterios de eliminación, y una vez que lo hemos encontrado, solicitamos eliminarlo de la colección original, lo que implicaría un segundo trabajo de iteración para buscar este elemento con el fin de eliminarlo.
  • Creo que vale la pena mencionar que el método remove de la Iteratorinterfaz está marcado como "opcional" en Javadocs, lo que significa que podría haber Iteratorimplementaciones que se descarten UnsupportedOperationExceptionsi invocamos el método remove. Como tal, diría que este enfoque es menos seguro que otros si no podemos garantizar el soporte del iterador para la eliminación de elementos.
Edwin Dalorzo avatar May 03 '2012 13:05 Edwin Dalorzo

Favorito de los veteranos (todavía funciona):

List<String> list;

for(int i = list.size() - 1; i >= 0; --i) 
{
        if(list.get(i).contains("bad"))
        {
                list.remove(i);
        }
}

Beneficios:

  1. Sólo itera sobre la lista una vez.
  2. No se crean objetos adicionales ni otra complejidad innecesaria
  3. No hay problemas al intentar utilizar el índice de un elemento eliminado, porque... bueno, ¡piénsalo!
Shebla Tsama avatar Mar 26 '2020 00:03 Shebla Tsama

En Java 8, existe otro enfoque. Colección#removeIf

p.ej:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

list.removeIf(i -> i > 2);
Santhosh avatar Oct 04 '2017 07:10 Santhosh

¿Hay alguna razón para preferir un enfoque sobre el otro?

El primer enfoque funcionará, pero tiene la evidente sobrecarga de copiar la lista.

El segundo enfoque no funcionará porque muchos contenedores no permiten modificaciones durante la iteración. Esto incluyeArrayList .

Si la única modificación es eliminar el elemento actual, puede hacer que el segundo enfoque funcione usando itr.remove()(es decir, use el método del iterador , no el del contenedor ). Este sería mi método preferido para iteradores que admitan .remove()remove()

NPE avatar May 03 '2012 13:05 NPE