Eliminar elementos de la colección mientras se itera
AFAIK, hay dos enfoques:
- Iterar sobre una copia de la colección.
- 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)?
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 addAll
método al final.
Usando ListIterator
Si está trabajando con listas, otra técnica consiste en utilizar una ListIterator
que 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 add
mé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 removeIf
método en la Collection
clase 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 removeAll
los 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.subSet
el 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
removeAl
la técnica funcionan con cualquier Colección (Colección, Lista, Conjunto, etc.). - Obviamente, la
ListIterator
técnica solo funciona con listas, siempre que suListIterator
implementación dada ofrezca soporte para operaciones de agregar y eliminar. - El
Iterator
enfoque funcionaría con cualquier tipo de colección, pero solo admite operaciones de eliminación. - Con el enfoque
ListIterator
/Iterator
la 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
removeAll
enfoque, 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
Iterator
interfaz está marcado como "opcional" en Javadocs, lo que significa que podría haberIterator
implementaciones que se descartenUnsupportedOperationException
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.
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:
- Sólo itera sobre la lista una vez.
- No se crean objetos adicionales ni otra complejidad innecesaria
- No hay problemas al intentar utilizar el índice de un elemento eliminado, porque... bueno, ¡piénsalo!
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);
¿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()