Java ListIterator
Recorre listas Java en ambas direcciones y modifícalas durante la iteración con la interfaz ListIterator.
ListIterator<E> extiende Iterator<E> con todo lo que una lista puede soportar y que un iterable genérico no puede: recorrer hacia atrás, consultar el índice actual, y agregar o reemplazar elementos durante la iteración. Está disponible en cualquier List<E> mediante list.listIterator() y list.listIterator(int startAt).
Si estás iterando un Set o una Queue, este capítulo no aplica — esas colecciones no tienen posiciones. Para List, ListIterator es el cursor que hace todo lo que el Iterator simple hace, más las cuatro operaciones específicas de listas.
Qué agrega ListIterator
public interface ListIterator<E> extends Iterator<E> {
// inherited:
boolean hasNext();
E next();
void remove();
// new:
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}Tres nuevas capacidades:
- Recorrido bidireccional.
hasPrevious()/previous()mueven el cursor hacia atrás.previous()lanzaNoSuchElementExceptional pasar el inicio. - Reporte de posición.
nextIndex()devuelve el índice que devolveríanext();previousIndex()devuelve el índice que devolveríaprevious(). Difieren en 1. - Ediciones en sitio.
set(e)reemplaza el elemento devuelto más recientemente pornextoprevious.add(e)inserta un nuevo elemento entre las posiciones anterior y siguiente del cursor.
El modelo del cursor
La clave para entender ListIterator es imaginar el cursor ubicado entre los elementos, no sobre ellos:
[ "a" "b" "c" ]
^ ^ ^ ^
0 1 2 3 <- nextIndex() valuesnext() devuelve el elemento a la derecha del cursor y avanza. previous() devuelve el elemento a la izquierda y retrocede. Justo después de que next() devuelve "b":
[ "a" "b" "c" ]
^
previousIndex()=1, nextIndex()=2Un set("B") posterior reemplaza "b". Un add("x") posterior inserta "x" entre "b" y "c". Un remove() posterior elimina "b". Solo uno de set, add o remove puede llamarse una vez por next/previous — llamar dos seguidos, o llamar cualquiera sin un next/previous intermedio, lanza IllegalStateException.
Recorrido bidireccional
List<String> letters = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> it = letters.listIterator();
while (it.hasNext()) System.out.print(it.next() + " "); // a b c
while (it.hasPrevious()) System.out.print(it.previous() + " "); // c b aAmbos bucles usan el mismo iterador. Cuando el bucle hacia adelante termina, el cursor está después de "c"; el bucle hacia atrás comienza ahí y regresa. También puedes cambiar de dirección a mitad del recorrido — llama a next() luego a previous() y obtienes el mismo elemento de vuelta ambas veces, porque el cursor avanzó más allá de él y luego retrocedió.
Edición en sitio durante la iteración
Esta es la razón principal para usar ListIterator en lugar de un Iterator simple:
List<String> words = new ArrayList<>(List.of("alpha", "beta", "gamma"));
ListIterator<String> it = words.listIterator();
while (it.hasNext()) {
String w = it.next();
if (w.startsWith("a")) it.set(w.toUpperCase()); // replace in place
if (w.equals("beta")) it.add("BETA-extra"); // insert after beta
}
// words is now [ALPHA, beta, BETA-extra, gamma]set es la única forma segura de reemplazar un elemento durante la iteración. add es la única forma segura de insertar un elemento durante la iteración. Ambos actualizan el contador interno de modificaciones esperadas del iterador, por lo que ninguno lanza una ConcurrentModificationException.
add merece un segundo análisis: inserta en el cursor — entre el resultado del next más reciente y el resultado del próximo next. Tras la inserción, el cursor queda después del nuevo elemento, por lo que el siguiente it.next() devuelve el elemento original siguiente, no el que acabas de agregar. Ese es casi siempre el comportamiento que deseas cuando "expandes" un elemento en su lugar.
Un error común: previous() devuelve el mismo elemento que acabas de devolver con next()
ListIterator<String> it = letters.listIterator();
it.next(); // "a", cursor between a and b
it.previous(); // "a" again, cursor between (start) and aEsto confunde a la gente. La posición del cursor cambia después de next, pero previous camina de vuelta sobre el mismo elemento. Si quieres el elemento anterior al actual, debes llamar a previous dos veces — una para retroceder sobre lo que acabas de devolver, y otra para leer el anterior real.
Comenzar en un índice específico
ListIterator<String> it = list.listIterator(3); // start with cursor before index 3La forma de dos argumentos posiciona el cursor antes del índice indicado. it.nextIndex() devuelve 3, it.previousIndex() devuelve 2, y el primer next() devuelve list.get(3). Es útil cuando ya localizaste un punto de inicio con indexOf o binarySearch y quieres recorrer desde ahí en cualquier dirección.
LinkedList vs ArrayList: misma interfaz, diferente costo
Ambos exponen ListIterator. El perfil de costo difiere:
ArrayList—next/previousson O(1);add/removedurante la iteración son O(n) porque desplazan el resto del arreglo. La operaciónsetse mantiene en O(1).LinkedList—next/previousson O(1) (el iterador cachea el nodo);add/removevía el iterador son O(1) porque no hay desplazamiento. Las mismas operaciones vía índice en unLinkedListson O(n) — las búsquedas por índice recorren la cadena.
Si te encuentras iterando un LinkedList y llamando list.add(index, ...) dentro del bucle, estás recorriendo la cadena dos veces por inserción. Usa el ListIterator y pagas O(1) por operación, que es la razón de ser de LinkedList.
Un ejemplo completo: recorrido bidireccional, ediciones en sitio, reporte de índices, operaciones con costo
El programa a continuación recorre una lista hacia adelante y hacia atrás con el mismo iterador, reemplaza e inserta elementos en sitio, reporta índices a medida que avanza, y mide la diferencia entre la modificación basada en iteradores y la basada en índices en un LinkedList.
Lo que se puede extraer de la ejecución:
- Los recorridos hacia adelante y hacia atrás ocurren en el mismo
ListIterator. Una vez que el bucle hacia adelante agotahasNext, el cursor está después del último elemento yhasPreviousse vuelve verdadero. setreemplazó"alpha"por"ALPHA"yadd("BETA-extra")insertó un nuevo elemento justo después de"beta"— y el iterador sobrevivió ambas modificaciones sin unaConcurrentModificationException.next()luegoprevious()devolvieron el mismo elemento. El cursor avanzó más allá de él y luego retrocedió; lo que parecía dos lecturas de elementos "diferentes" es en realidad un elemento recorrido dos veces.- En un
LinkedList, la versión basada en iteradores de "eliminar cada otro elemento" fue dramáticamente más rápida que la versión basada en índices. La búsqueda por índice en una lista enlazada es O(n); el iterador cachea su nodo y la eliminación es O(1).
Qué sigue
Iterator y ListIterator se encargan del lado del recorrido al trabajar con colecciones. La otra mitad de "hacer cosas con elementos" es ordenarlos: decirle a Java cuándo un elemento es menor, igual o mayor que otro. Eso es lo que cubren Comparable y Comparator — el orden natural incorporado en el tipo mismo, y los ordenamientos externos que proporcionas por operación. Son la base sobre la que se apoya todo lo demás en esta parte del libro, incluidas las utilidades de ordenamiento y búsqueda.