Pular para o conteúdo

O Iterator é um padrão de projeto comportamental que permite percorrer os elementos de uma coleção sem expor os detalhes de como essa coleção está organizada internamente.

Na prática, ele separa a responsabilidade de armazenar dados da responsabilidade de percorrer dados. Assim, o cliente consegue navegar por listas, pilhas, árvores ou outras estruturas usando uma interface comum.

Esse padrão existe para reduzir acoplamento, facilitar a troca de algoritmos de travessia e evitar que a lógica de iteração fique espalhada pelo sistema.

Imagine um sistema acadêmico que precisa trabalhar com diferentes coleções de dados:

  • uma lista de alunos matriculados
  • uma pilha de solicitações recentes
  • uma árvore de disciplinas e pré-requisitos
  • uma coleção filtrada de notificações

Cada estrutura pode exigir um jeito diferente de travessia. Em alguns casos, a navegação é linear. Em outros, pode ser em profundidade, em largura ou até baseada em filtros.

Se o código cliente precisar conhecer a estrutura interna de cada coleção, surgem vários problemas:

  • aumento do acoplamento com classes concretas
  • duplicação de lógica de travessia
  • dificuldade para trocar o tipo de coleção
  • risco de expor detalhes internos desnecessários
  • maior esforço para adicionar novos modos de iteração

O problema central é este: o cliente quer acessar os elementos, mas não deveria depender de como a coleção os armazena ou percorre.

O Iterator resolve isso extraindo a lógica de travessia para um objeto separado: o iterador.

Em vez de o cliente percorrer diretamente a coleção, ele pede a ela um iterador e usa operações simples para avançar.

Uma forma de entender a solução é esta:

  1. a coleção continua responsável por armazenar os elementos
  2. o iterador passa a ser responsável por controlar a travessia
  3. o cliente conversa com a interface do iterador, não com a estrutura interna
  4. diferentes iteradores podem oferecer diferentes estratégias de navegação
  5. múltiplos iteradores podem percorrer a mesma coleção ao mesmo tempo, cada um com seu próprio estado

Com isso, a coleção fica mais coesa, o cliente fica menos acoplado e a iteração passa a ser reutilizável e extensível.

Pense em uma biblioteca universitária. Os livros estão organizados em estantes, corredores e seções. Um estudante não precisa conhecer toda a lógica interna de catalogação para consultar os livros. Ele pode usar um catálogo, um mapa de busca ou a ajuda de um bibliotecário.

Cada uma dessas opções oferece uma forma de percorrer a mesma coleção de livros. A biblioteca continua sendo a coleção. O catálogo, o mapa ou o bibliotecário funcionam como iteradores diferentes sobre esse mesmo conjunto.

Use Iterator quando:

  • a coleção possui estrutura interna que você não quer expor
  • existem vários modos possíveis de travessia
  • o cliente precisa percorrer diferentes coleções por uma interface comum
  • você quer reduzir duplicação de código de iteração
  • a travessia precisa manter estado independente por execução

Exemplos reais em software:

  • percorrer resultados paginados de uma API
  • navegar por árvore de menus ou categorias
  • iterar sobre componentes de uma interface gráfica
  • processar nós de uma árvore sintática
  • percorrer conexões de um grafo social ou acadêmico

Iterator pode ser mal aplicado quando a equipe cria iteradores próprios para coleções muito simples sem ganho claro de legibilidade ou flexibilidade.

Erros frequentes:

  • criar abstrações demais para uma lista trivial
  • misturar regras de negócio dentro do iterador
  • fazer o iterador depender de detalhes externos demais
  • expor internamente a coleção mesmo dizendo que há encapsulamento
  • usar o padrão sem necessidade, apenas por seguir catálogo

As consequências costumam ser aumento de complexidade, excesso de classes e dificuldade de manutenção. O Iterator faz mais sentido quando realmente existe valor em separar travessia de armazenamento.

Uma implementação incremental pode seguir estes passos:

  1. identifique a coleção que precisa ser percorrida de maneira controlada
  2. defina uma interface de iterador com operações como temProximo() e proximo()
  3. implemente um iterador concreto que mantenha o estado da travessia
  4. faça a coleção expor um método para criar iteradores
  5. ajuste o cliente para usar o iterador em vez de acessar a estrutura diretamente
  6. se necessário, crie novos iteradores para percursos alternativos

Antes de implementar, vale perguntar: o problema está na coleção em si ou na variedade de formas de percorrê-la? Se a travessia estiver atrapalhando o design, Iterator é um bom candidato.

No exemplo abaixo, uma coleção de disciplinas expõe um iterador simples para percorrer seus elementos sem revelar a estrutura interna.

import java.util.ArrayList;
import java.util.List;
interface IteradorDisciplina {
boolean temProximo();
String proximo();
}
class GradeCurricular {
private final List<String> disciplinas = new ArrayList<>();
public void adicionar(String disciplina) {
disciplinas.add(disciplina);
}
public IteradorDisciplina criarIterador() {
return new GradeCurricularIterador(disciplinas);
}
}
class GradeCurricularIterador implements IteradorDisciplina {
private final List<String> disciplinas;
private int posicaoAtual = 0;
public GradeCurricularIterador(List<String> disciplinas) {
this.disciplinas = disciplinas;
}
@Override
public boolean temProximo() {
return posicaoAtual < disciplinas.size();
}
@Override
public String proximo() {
if (!temProximo()) {
throw new IllegalStateException("Nao ha mais disciplinas.");
}
return disciplinas.get(posicaoAtual++);
}
}

Nesse código, GradeCurricular cuida dos dados, enquanto GradeCurricularIterador cuida da navegação. O cliente não precisa saber se as disciplinas estão em uma lista, árvore ou outra estrutura.

  • reduz o acoplamento entre cliente e coleção concreta
  • centraliza a lógica de travessia
  • permite múltiplos algoritmos de iteração
  • facilita percursos paralelos sobre a mesma coleção
  • melhora extensibilidade sem alterar o cliente
  • adiciona mais classes e interfaces ao design
  • pode ser excessivo para coleções muito simples
  • pode introduzir alguma indireção extra
  • exige cuidado com estado interno do iterador
  • pode complicar depuração se houver muitos tipos de percurso

O Iterator se relaciona bem com outros padrões e ideias:

  • Composite: uma estrutura em árvore costuma precisar de iteradores para percorrer seus nós
  • Factory Method: a coleção pode usar um método-fábrica para retornar diferentes iteradores
  • Memento: pode ser usado para salvar e restaurar o estado de uma iteração
  • Visitor: Visitor executa operações nos elementos; Iterator ajuda a percorrê-los
  • Encapsulamento: o padrão reforça a ideia de esconder a representação interna da coleção

Essas relações mostram que o Iterator não serve para armazenar dados de modo diferente, mas para organizar melhor a forma de acesso aos elementos.

Algumas variações comuns incluem:

  • iteradores externos, em que o cliente controla explicitamente a travessia
  • iteradores internos, em que a coleção aplica uma função aos elementos
  • iteradores em profundidade e em largura para árvores
  • iteradores filtrados, paginados ou reversos
  • uso de iteradores nativos da linguagem, como Iterator em Java ou Iterable

Em muitos frameworks, a ideia do padrão já aparece embutida nas bibliotecas de coleção. Ainda assim, entender o padrão ajuda a projetar estruturas próprias quando o caso exige.

Agora veja um exemplo mais realista. Um sistema de aprendizagem precisa percorrer módulos de um curso de duas formas: na ordem padrão e na ordem inversa para revisão.

import java.util.ArrayList;
import java.util.List;
interface IteradorModulo {
boolean temProximo();
Modulo proximo();
}
class Modulo {
private final String titulo;
public Modulo(String titulo) {
this.titulo = titulo;
}
public String getTitulo() {
return titulo;
}
}
class Curso {
private final List<Modulo> modulos = new ArrayList<>();
public void adicionarModulo(Modulo modulo) {
modulos.add(modulo);
}
public IteradorModulo criarIteradorPadrao() {
return new IteradorCursoPadrao(modulos);
}
public IteradorModulo criarIteradorReverso() {
return new IteradorCursoReverso(modulos);
}
}
class IteradorCursoPadrao implements IteradorModulo {
private final List<Modulo> modulos;
private int indice = 0;
public IteradorCursoPadrao(List<Modulo> modulos) {
this.modulos = modulos;
}
public boolean temProximo() {
return indice < modulos.size();
}
public Modulo proximo() {
return modulos.get(indice++);
}
}
class IteradorCursoReverso implements IteradorModulo {
private final List<Modulo> modulos;
private int indice;
public IteradorCursoReverso(List<Modulo> modulos) {
this.modulos = modulos;
this.indice = modulos.size() - 1;
}
public boolean temProximo() {
return indice >= 0;
}
public Modulo proximo() {
return modulos.get(indice--);
}
}
public class Main {
public static void main(String[] args) {
Curso curso = new Curso();
curso.adicionarModulo(new Modulo("Introducao"));
curso.adicionarModulo(new Modulo("Colecoes"));
curso.adicionarModulo(new Modulo("Iterator"));
IteradorModulo padrao = curso.criarIteradorPadrao();
while (padrao.temProximo()) {
System.out.println("Ordem normal: " + padrao.proximo().getTitulo());
}
IteradorModulo reverso = curso.criarIteradorReverso();
while (reverso.temProximo()) {
System.out.println("Revisao: " + reverso.proximo().getTitulo());
}
}
}

Nesse exemplo, o mesmo objeto Curso oferece dois modos de travessia sem expor como os módulos estão armazenados. O cliente muda o comportamento apenas escolhendo outro iterador.