Iteradores Java
Percorra coleções Java com a interface Iterator — hasNext, next, remove — e o contrato Iterable.
Toda vez que você escreve for (T x : collection) em Java, está usando um par oculto: a interface Iterable<T>, que permite que a coleção seja percorrida em loop, e o Iterator<T> que ela entrega ao loop. O for-each é açúcar sintático; o Iterator é o motor. Entender o que ele faz — e o que seus três métodos podem lançar — é a diferença entre "minha travessia de lista funciona na maioria das vezes" e "sei exatamente quando ela vai falhar."
Este capítulo trata do Iterator<E> simples. O mais rico ListIterator<E>, que pode caminhar para trás e modificar durante a iteração, tem seu próprio capítulo a seguir.
As duas interfaces
Iterable<T> é o contrato para "coisas que podem ser iteradas":
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { ... }
default Spliterator<T> spliterator() { ... }
}Iterator<T> é o cursor:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException(); }
default void forEachRemaining(Consumer<? super E> action) { ... }
}Todo Collection<E> estende Iterable<E> — é por isso que um loop for-each funciona em uma List, um Set, uma Queue. (Um Map não é Iterable; você itera sobre seu entrySet(), keySet() ou values() — veja como iterar um HashMap.) O loop for-each:
for (String name : names) { System.out.println(name); }é compilado para:
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}Depois de ver essa desaçucarização, o restante das regras do Iterator faz sentido.
Os três métodos e o que eles lançam
hasNext() retorna true se next() teria sucesso. Idempotente — chamá-lo duas vezes seguidas é seguro. Nunca lança exceção (em implementações bem-comportadas).
next() avança o cursor e retorna o elemento. Lança NoSuchElementException se não há próximo elemento. Este é o único método do iterator que lança uma exceção por design quando usado incorretamente. Sempre proteja com hasNext() se houver qualquer chance de a coleção estar vazia:
while (it.hasNext()) { use(it.next()); } // safe patternremove() remove o elemento retornado mais recentemente por next(). É um método default que lança UnsupportedOperationException a menos que o iterator o implemente. ArrayList, HashMap.keySet().iterator() e similares suportam isso. Iteradores retornados de List.of(...), Collections.unmodifiableList(...) e stream .iterator() não suportam. Você também não pode chamar remove() duas vezes seguidas sem um next() intermediário — isso lança IllegalStateException.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.isEmpty()) it.remove(); // legal, fail-safe removal
}it.remove() é a única forma segura de remover de uma coleção durante a iteração com um Iterator simples. O próprio método remove(...) da coleção invalidaria o iterator e lançaria ConcurrentModificationException na próxima chamada.
Iteração fail-fast
A maioria dos iteradores de coleções do JDK é fail-fast: eles registram a contagem de modificações da coleção quando o iterator é criado, verificam a cada chamada de hasNext/next, e lançam ConcurrentModificationException se ela tiver sido alterada por qualquer outra coisa além do próprio iterator.
List<String> names = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = names.iterator();
names.add("d"); // direct mutation, not via iterator
it.next(); // throws ConcurrentModificationExceptionFail-fast é um diagnóstico de melhor esforço, não uma garantia de segurança de threads. Ele captura o erro comum ("ah, modifiquei a lista dentro do loop e agora meu iterator está confuso") de forma clara e antecipada. Ele não protege contra modificações concorrentes de outra thread — para isso você precisa de uma coleção concorrente (CopyOnWriteArrayList, ConcurrentHashMap) cujos iteradores são fracamente consistentes: eles percorrem um snapshot e nunca lançam exceção.
forEachRemaining e forEach
Dois métodos default tornam a iteração mais curta quando você não precisa do cursor:
list.forEach(System.out::println); // every element
Iterator<String> it = list.iterator();
while (it.hasNext() && !it.next().equals("STOP")) { }
it.forEachRemaining(System.out::println); // everything past STOPforEach está em Iterable; forEachRemaining está em Iterator. Ambos são sequenciais. Não os use quando você também precisar de remove — eles ocultam o cursor, e remove o requer.
Escrevendo seu próprio Iterator
Você vai escrever um quando implementar um tipo semelhante a uma coleção personalizada. O contrato é pequeno, mas cada parte dele importa:
class Countdown implements Iterable<Integer> {
private final int from;
Countdown(int from) { this.from = from; }
@Override public Iterator<Integer> iterator() {
return new Iterator<>() {
int n = from;
@Override public boolean hasNext() { return n > 0; }
@Override public Integer next() {
if (n <= 0) throw new NoSuchElementException();
return n--;
}
};
}
}
for (int x : new Countdown(3)) System.out.println(x); // 3 2 1Três coisas para acertar:
next()deve lançarNoSuchElementExceptionquando esgotado. Não retornenullou algum sentinela.- O iterator deve ser uma nova instância a cada chamada de
iterator(). Chamarfor (... : it)duas vezes no mesmo iterable deve começar do início em ambos os casos. remove()é opcional. Não o implemente a menos que você realmente possa — o corpodefaultque lança exceção está correto.
Um exemplo trabalhado: iteração, remoção, fail-fast, iterable personalizado
O programa abaixo percorre um ArrayList de três formas (for-each, iterator explícito, forEachRemaining), remove elementos com segurança usando Iterator.remove, demonstra a exceção fail-fast quando você contorna o iterator, e termina com um pequeno Iterable<T> personalizado.
O que extrair da execução:
- O loop for-each imprimiu cada elemento; por baixo dos panos, ele pediu ao
ArrayListum iterator e o percorreu comhasNext/next. Iterator.removedeletou as strings vazias durante a iteração sem umaConcurrentModificationException. Essa é a única técnica correta de exclusão dentro do loop com umIteratorsimples.forEachRemainingé uma forma elegante de esvaziar o que o iterator ainda não produziu — útil logo após uma travessia parcial.- Mutar a lista diretamente enquanto outro iterator estava ativo lançou
ConcurrentModificationExceptionna próxima chamada denext(). A exceção é intencional: ela torna o erro visível. - O
Countdownpersonalizado mostra o contrato mínimo necessário para escrever um iterable funcional.hasNextreporta claramente;nextlança quando esgotado; semremove(herda o padrão).
O que vem a seguir
Um Iterator simples pode avançar e remover. Isso é suficiente para sets, maps e queues — eles não têm posições em nenhum outro sentido. Listas têm, e recebem um cursor mais rico: ListIterator<E> pode mover para frente e para trás, reportar índices, e add ou set elementos durante o percurso. Esse é o próximo capítulo.