W3docs

Java ListIterator

Percorra listas Java em ambas as direções e modifique durante a iteração com a interface ListIterator.

ListIterator<E> estende Iterator<E> com tudo que uma lista suporta além do que um iterável genérico oferece: caminhar para trás, consultar o índice atual e adicionar ou substituir elementos durante a iteração. Está disponível em todo List<E> via list.listIterator() e list.listIterator(int startAt).

Se você estiver iterando um Set ou uma Queue, este capítulo não se aplica — essas coleções não têm posições. Para List, o ListIterator é o cursor que faz tudo que o simples Iterator faz, mais as quatro operações específicas de lista.

O que ListIterator adiciona

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

Três novas capacidades:

  1. Percurso bidirecional. hasPrevious() / previous() movem o cursor para trás. previous() lança NoSuchElementException quando vai além do início.
  2. Relatório de posição. nextIndex() retorna o índice que next() retornaria; previousIndex() retorna o índice que previous() retornaria. Eles diferem por 1.
  3. Edições no lugar. set(e) substitui o elemento retornado mais recentemente por next ou previous. add(e) insere um novo elemento entre as posições anterior e próxima do cursor.

O modelo de cursor

O segredo para entender o ListIterator é imaginar o cursor sentado entre os elementos, não sobre eles:

       [ "a"   "b"   "c" ]
        ^     ^     ^     ^
        0     1     2     3      <- nextIndex() values

next() retorna o elemento à direita do cursor e avança. previous() retorna o elemento à esquerda e recua. Logo após next() retornar "b":

       [ "a"   "b"   "c" ]
                    ^
              previousIndex()=1, nextIndex()=2

Um set("B") subsequente substitui "b". Um add("x") subsequente insere "x" entre "b" e "c". Um remove() subsequente deleta "b". Apenas um entre set, add ou remove pode ser chamado uma vez por next/previous — chamar dois em sequência, ou chamar qualquer um deles sem um next/previous intermediário, lança IllegalStateException.

Percurso bidirecional

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 os laços usam o mesmo iterador. Após o laço direto terminar, o cursor está além de "c"; o laço reverso começa ali e volta ao início. Você também pode mudar de direção no meio do percurso — chame next() e depois previous() e você obtém o mesmo elemento de volta nas duas vezes, pois o cursor se moveu além dele e depois voltou.

Edição no lugar durante a iteração

Esta é a principal razão para usar ListIterator em vez de um simples Iterator:

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 é a única forma segura de substituir um elemento durante a iteração. add é a única forma segura de inserir um elemento durante a iteração. Ambos atualizam a contagem de modificações esperadas interna do iterador, portanto nenhum deles aciona uma ConcurrentModificationException.

add merece uma segunda análise: ele insere no cursor — entre o resultado do next mais recente e o próximo resultado de next. Após a inserção, o cursor está além do novo elemento, então o próximo it.next() retorna o elemento original seguinte, não o que você acabou de adicionar. Esse é quase sempre o comportamento desejado quando você está "expandindo" um elemento no lugar.

Uma armadilha comum: previous() retorna o mesmo elemento que você acabou de retornar com next()

ListIterator<String> it = letters.listIterator();
it.next();      // "a", cursor between a and b
it.previous();  // "a" again, cursor between (start) and a

Isso confunde muita gente. A posição do cursor muda após next, mas previous volta sobre o mesmo elemento. Se você quer o elemento antes do atual, é necessário chamar previous duas vezes — uma para recuar sobre o que acabou de retornar, outra para efetivamente ler o anterior.

Começando em um índice específico

ListIterator<String> it = list.listIterator(3);    // start with cursor before index 3

A forma com dois argumentos posiciona o cursor antes do índice fornecido. it.nextIndex() retorna 3, it.previousIndex() retorna 2, e o primeiro next() retorna list.get(3). Útil quando você já localizou um ponto de partida com indexOf ou binarySearch e quer caminhar a partir daí em qualquer direção.

LinkedList vs ArrayList: mesma interface, custo diferente

Ambas expõem ListIterator. O perfil de custo difere:

  • ArrayListnext/previous são O(1); add/remove durante a iteração são O(n) porque deslocam o final do array. A operação set permanece O(1).
  • LinkedListnext/previous são O(1) (o iterador armazena o nó em cache); add/remove via iterador são O(1) porque não há deslocamento. As mesmas operações via índice em uma LinkedList são O(n) — consultas por índice percorrem a cadeia.

Se você estiver iterando uma LinkedList e chamando list.add(index, ...) dentro do laço, você está percorrendo a cadeia duas vezes por inserção. Use o ListIterator e você paga O(1) por operação, que é exatamente o motivo pelo qual LinkedList existe.

Um exemplo completo: percurso bidirecional, edições no lugar, relatório de índices, operações com custo

O programa abaixo percorre uma lista para frente e para trás no mesmo iterador, substitui e insere elementos no lugar, relata índices ao longo do caminho e mede a diferença entre modificação baseada em iterador e baseada em índice em uma LinkedList.

java— editable, runs on the server

O que observar na execução:

  • Os percursos direto e reverso ocorrem no mesmo ListIterator. Quando o laço direto esgota hasNext, o cursor está além do último elemento e hasPrevious se torna verdadeiro.
  • set substituiu "alpha" por "ALPHA" e add("BETA-extra") inseriu um novo elemento logo após "beta" — e o iterador sobreviveu a ambas as modificações sem uma ConcurrentModificationException.
  • next() seguido de previous() retornou o mesmo elemento. O cursor se moveu além dele e depois voltou; o que parecia duas leituras de elementos "diferentes" é na verdade um elemento percorrido duas vezes.
  • Em uma LinkedList, a versão baseada em iterador de "remover um a cada dois elementos" foi dramaticamente mais rápida do que a versão baseada em índice. Consulta por índice em uma lista encadeada é O(n); o iterador armazena seu nó em cache e a exclusão é O(1).

O que vem a seguir

Iterator e ListIterator tratam do lado de percurso ao trabalhar com coleções. A outra metade de "fazer coisas com elementos" é ordená-los: dizer ao Java quando um elemento é menor, igual ou maior que outro. É isso que Comparable e Comparator abordam — ordem natural embutida no tipo em si e ordenações externas fornecidas por operação. Eles são a base sobre a qual tudo mais nesta parte do livro repousa, incluindo os utilitários de ordenação e busca.

Prática

Prática
Você chama `ListIterator<String> it = list.listIterator()`, depois `it.next()`, depois `it.add('x')`. O que a próxima chamada a `it.next()` retorna?
Você chama `ListIterator<String> it = list.listIterator()`, depois `it.next()`, depois `it.add('x')`. O que a próxima chamada a `it.next()` retorna?
Was this page helpful?