Eliminar elementos de la colección mientras se itera


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 la legibilidad)?

Author: user1329572, 2012-05-03

8 answers

Permítanme dar algunos ejemplos con algunas alternativas para evitar un 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 Retirar

La primera técnica consiste en recopilar todos los objetos que queremos eliminar (por ejemplo, usando 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<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 hacer es "eliminar".

Si quieres " añadir" este enfoque también funcionaría, pero asumiría que iteraría sobre una colección diferente para determinar qué elementos desea agregar a una segunda colección y luego emitiría 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 la eliminación y adición de elementos durante la iteración en sí.

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

Nuevamente, usé el método" remove " en el ejemplo anterior que es lo que su pregunta parecía implicar, pero también puede usar su método add 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 podría utilizar para tomar ventaja de ella.

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

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

O use la nueva API de stream:

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, reasigne la referencia original a la colección filtrada (es decir, books = filtered) o utilice la colección filtrada a removeAll los elementos encontrados de la colección original (es decir, books.removeAll(filtered)).

Use Sublista o Subconjunto

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();

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

Algo similar podría lograrse con conjuntos ordenados usando el método NavigableSet.subSet, o cualquiera de los métodos de corte ofrecidos allí.

Consideraciones:

El método que utilice puede depender de lo que tenga la intención de hacer

  • La técnica collect y removeAl funciona con cualquier Colección (Colección, Lista, Conjunto, etc.).
  • La técnica ListIterator obviamente solo funciona con listas, siempre que su dado ListIterator la implementación ofrece soporte para agregar y quitar operaciones.
  • El enfoque Iterator funcionaría con cualquier tipo de colección, pero solo admite operaciones de eliminación.
  • Con el ListIterator/Iterator enfoque la ventaja obvia es no tener que copiar nada, ya que eliminamos a medida que iteramos. Por lo tanto, esto es muy eficiente.
  • El ejemplo de flujos JDK 8 no elimina nada, sino que busca los elementos deseados, y luego reemplazamos la referencia de la colección original con la nueva, y que la vieja sea recogida de basura. Por lo tanto, iteramos solo una vez sobre la colección y eso sería eficiente.
  • En el enfoque collect y removeAll 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 hayamos encontrado, pedimos 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.
  • I 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 Iterator implementaciones 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.
 244
Author: Edwin Dalorzo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-25 13:34:25

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

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

El segundo enfoque no funcionará porque muchos contenedores no permiten la modificación 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, utilice el iterador remove() método, no el contenedor's). Este sería mi método preferido para los iteradores que soportan remove().

 11
Author: NPE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:34

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

Eg:

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

list.removeIf(i -> i > 2);
 9
Author: Santhosh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-04 07:19:48

Solo un segundo enfoque funcionará. Puede modificar la colección durante la iteración usando iterator.remove() solamente. Todos los demás intentos causarán ConcurrentModificationException.

 4
Author: AlexR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-03 13:07:22

No puedes hacer el segundo, porque incluso si usas el método remove() en el Iterador , obtendrás una excepción lanzada .

Personalmente, preferiría la primera para todas las instancias Collection, a pesar de que se escuchó por casualidad la creación de la nueva Collection, me parece menos propensa a errores durante la edición por otros desarrolladores. En algunas implementaciones de Colecciones, el Iterador remove() es compatible, en otras no. Puede leer más en los documentos de Iterador.

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

 1
Author: Jon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-03 13:42:22

Elegiría el segundo ya que no tienes que hacer una copia de la memoria y el Iterador funciona más rápido. Así que ahorra memoria y tiempo.

 0
Author: Calin Andrei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-03 13:08:27

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

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

Simplemente conciste un 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 se requiere tener una lista mutable, pero me gusta la simplicidad de esta solución.

 0
Author: AxelH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-02 11:32:06

¿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 keyset ()

 -1
Author: Drake Clarris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-03 13:41:17