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:
- Percurso bidirecional.
hasPrevious()/previous()movem o cursor para trás.previous()lançaNoSuchElementExceptionquando vai além do início. - Relatório de posição.
nextIndex()retorna o índice quenext()retornaria;previousIndex()retorna o índice queprevious()retornaria. Eles diferem por 1. - Edições no lugar.
set(e)substitui o elemento retornado mais recentemente pornextouprevious.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() valuesnext() 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()=2Um 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 aAmbos 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 aIsso 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 3A 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:
ArrayList—next/previoussão O(1);add/removedurante a iteração são O(n) porque deslocam o final do array. A operaçãosetpermanece O(1).LinkedList—next/previoussão O(1) (o iterador armazena o nó em cache);add/removevia iterador são O(1) porque não há deslocamento. As mesmas operações via índice em umaLinkedListsã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.
O que observar na execução:
- Os percursos direto e reverso ocorrem no mesmo
ListIterator. Quando o laço direto esgotahasNext, o cursor está além do último elemento ehasPreviousse torna verdadeiro. setsubstituiu"alpha"por"ALPHA"eadd("BETA-extra")inseriu um novo elemento logo após"beta"— e o iterador sobreviveu a ambas as modificações sem umaConcurrentModificationException.next()seguido deprevious()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.