Eliminar elementos de la colección mientras se itera

AFAIK, hay dos enfoques:

  1. Iterato sobre una copia de la colección
  2. Use el iterador de la colección actual

Por ejemplo,

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

y

 Iterator 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 el simple motivo de legibilidad)?

Permítanme dar algunos ejemplos con algunas alternativas para evitar una ConcurrentModificationException .

Supongamos que tenemos la siguiente colección de libros

 List books = new ArrayList(); 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"))); 

Recoge y quita

La primera técnica consiste en recostackr todos los objetos que queremos eliminar (por ejemplo, utilizando un bucle for mejorado) y después de terminar de iterar, eliminamos todos los objetos encontrados.

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

Esto supone que la operación que desea hacer es “eliminar”.

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

Usando ListIterator

Si está trabajando con listas, otra técnica consiste en usar un ListIterator que tiene soporte para eliminar y agregar elementos durante la iteración misma.

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

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

Usando JDK 8

Para aquellos que trabajan con Java 8 o versiones superiores, hay un par de otras técnicas que puede utilizar para aprovecharlo.

Puede usar el nuevo método removeIf en la clase base de Collection :

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

O use la nueva API de transmisión:

 ISBN other = new ISBN("0-201-63361-2"); List 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 para removeAll los elementos encontrados de la colección original (es decir, books.removeAll(filtered) )

Usar Sublist o Subset

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

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

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

Se podría lograr algo similar con los conjuntos ordenados que usan el método NavigableSet.subSet o cualquiera de los métodos de corte ofrecidos allí.

Consideraciones:

El método que use puede depender de lo que se propone hacer

  • La técnica de recostackción y removeAl funciona con cualquier colección (colección, lista, conjunto, etc.).
  • La técnica ListIterator obviamente solo funciona con listas, siempre que su implementación dada de ListIterator ofrezca soporte para operaciones de agregar y quitar.
  • El enfoque Iterator funcionaría con cualquier tipo de colección, pero solo admite operaciones de eliminación.
  • Con el enfoque ListIterator / ListIterator , la ventaja obvia es no tener que copiar nada, ya que lo eliminamos al iterar. Entonces, esto es muy eficiente.
  • El ejemplo de las streams JDK 8 en realidad no eliminó nada, pero buscó los elementos deseados, luego reemplazamos la referencia de la colección original con la nueva y dejamos que la anterior sea recolectada. Entonces, iteramos solo una vez sobre la colección y eso sería eficiente.
  • En el método de recostackción y removeAll , la desventaja es que debemos 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, le pedimos que lo elimine de la colección original, lo que implicaría una segunda iteración para buscar este elemento con el fin de eliminarlo
  • Creo que vale la pena mencionar que el método remove de la interfaz Iterator está marcado como “opcional” en Javadocs, lo que significa que podría haber implementaciones Iterator que arrojen UnsupportedOperationException si 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.

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

El primer enfoque funcionará, pero tiene la carga obvia de copiar la lista.

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

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

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

p.ej:

 List list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); list.removeIf(i -> i > 2); 

Solo el segundo enfoque funcionará. Puede modificar la recostackción durante la iteración utilizando solo iterator.remove() . Todos los demás bashs causarán ConcurrentModificationException .

No puede hacer el segundo, porque incluso si usa el método remove() en Iterator , obtendrá una Excepción lanzada .

Personalmente, preferiría la primera para todas las instancias de la Collection , a pesar de la escucha adicional de crear la nueva Collection , creo que es menos propenso a errores durante la edición de otros desarrolladores. En algunas implementaciones de Colección, Iterator remove() es compatible, en otras no lo es. Puede leer más en los documentos para Iterator .

La tercera alternativa es crear una nueva Collection , iterar sobre la original y agregar todos los miembros de la primera Collection a la segunda Collection que no están listos para su eliminación. Dependiendo del tamaño de la Collection y del número de eliminaciones, esto podría ahorrar significativamente en la memoria, en comparación con el primer enfoque.

Yo elegiría el segundo ya que no tiene que hacer una copia de la memoria y el iterador funciona más rápido. Así que ahorras memoria y tiempo.

por qué no esto?

 for( int i = 0; i < Foo.size(); i++ ) { if( Foo.get(i).equals( some test ) ) { Foo.remove(i); } } 

Y si es un mapa, no una lista, puede usar el conjunto de claves ()

También hay una solución simple para “iterar” una Collection y eliminar cada elemento.

 List list = new ArrayList<>(); //Fill the list 

Simplemente conciste en el bucle hasta que la lista esté vacía, y en cada iteración, eliminamos el primer elemento con remove(0) .

 while(!list.isEmpty()){ String s = list.remove(0); // do you thing } 

No creo que esto tenga ninguna mejora en comparación con el Iterator , todavía requiere tener una lista mutable, pero me gusta la simplicidad de esta solución.