W3docs

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 pattern

remove() 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 ConcurrentModificationException

Fail-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 STOP

forEach 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 1

Três coisas para acertar:

  1. next() deve lançar NoSuchElementException quando esgotado. Não retorne null ou algum sentinela.
  2. O iterator deve ser uma nova instância a cada chamada de iterator(). Chamar for (... : it) duas vezes no mesmo iterable deve começar do início em ambos os casos.
  3. remove() é opcional. Não o implemente a menos que você realmente possa — o corpo default que 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.

java— editable, runs on the server

O que extrair da execução:

  • O loop for-each imprimiu cada elemento; por baixo dos panos, ele pediu ao ArrayList um iterator e o percorreu com hasNext/next.
  • Iterator.remove deletou as strings vazias durante a iteração sem uma ConcurrentModificationException. Essa é a única técnica correta de exclusão dentro do loop com um Iterator simples.
  • 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 ConcurrentModificationException na próxima chamada de next(). A exceção é intencional: ela torna o erro visível.
  • O Countdown personalizado mostra o contrato mínimo necessário para escrever um iterable funcional. hasNext reporta claramente; next lança quando esgotado; sem remove (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.

Prática

Prática
Dentro de um loop `for-each` sobre `List<String> list`, você chama `list.remove(name)` para remover entradas correspondentes. A primeira remoção funciona; a próxima iteração lança exceção. Qual é a correção certa?
Dentro de um loop `for-each` sobre `List<String> list`, você chama `list.remove(name)` para remover entradas correspondentes. A primeira remoção funciona; a próxima iteração lança exceção. Qual é a correção certa?
Was this page helpful?