W3docs

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:

  1. Recorrido bidireccional. hasPrevious() / previous() mueven el cursor hacia atrás. previous() lanza NoSuchElementException al pasar el inicio.
  2. Reporte de posición. nextIndex() devuelve el índice que devolvería next(); previousIndex() devuelve el índice que devolvería previous(). Difieren en 1.
  3. Ediciones en sitio. set(e) reemplaza el elemento devuelto más recientemente por next o previous. 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() values

next() 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()=2

Un 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 a

Ambos 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 a

Esto 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 3

La 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:

  • ArrayListnext/previous son O(1); add/remove durante la iteración son O(n) porque desplazan el resto del arreglo. La operación set se mantiene en O(1).
  • LinkedListnext/previous son O(1) (el iterador cachea el nodo); add/remove vía el iterador son O(1) porque no hay desplazamiento. Las mismas operaciones vía índice en un LinkedList son 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.

java— editable, runs on the server

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 agota hasNext, el cursor está después del último elemento y hasPrevious se vuelve verdadero.
  • set reemplazó "alpha" por "ALPHA" y add("BETA-extra") insertó un nuevo elemento justo después de "beta" — y el iterador sobrevivió ambas modificaciones sin una ConcurrentModificationException.
  • next() luego previous() 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.

Práctica

Práctica
Llamas a `ListIterator<String> it = list.listIterator()`, luego `it.next()`, luego `it.add('x')`. ¿Qué devuelve la siguiente llamada a `it.next()`?
Llamas a `ListIterator<String> it = list.listIterator()`, luego `it.next()`, luego `it.add('x')`. ¿Qué devuelve la siguiente llamada a `it.next()`?
Was this page helpful?