1. O problema da abordagem imperativa
Vamos começar com um problema clássico. Suponha que temos uma lista de usuários:
List<User> users = ...; // já preenchida
Digamos que precisamos obter a lista de nomes de todos os usuários maiores de idade (a partir de 18 anos). Como fazíamos isso antes?
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
names.add(user.getName());
}
}
À primeira vista, tudo parece simples, mas repare nos passos “técnicos”: declarar uma nova lista para o resultado, percorrer todos os elementos, verificar a condição, adicionar o resultado.
Se a tarefa se complica — por exemplo, precisamos obter uma lista em ordem alfabética de e‑mails únicos de todos os usuários cujos nomes começam com a letra “A” — então o código cresce rapidamente: filtragem, extração de campos, remoção de duplicatas, ordenação, possivelmente conversões no formato do e‑mail etc. No fim, em vez de uma lógica curta e clara, você acaba com uma montanha de boilerplate que é difícil de ler e manter.
As desvantagens dessa abordagem em poucas palavras: muito código repetitivo, alta probabilidade de cometer erros (esquecer a ordenação ou tratar duplicatas de forma incorreta) e dificuldade em combinar operações. Isso é o chamado estilo imperativo: você descreve ao computador, passo a passo, como executar a tarefa, e não o que você quer obter.
2. Stream API — estilo declarativo
Com o lançamento do Java 8 surgiu o Stream API — uma ferramenta que permite trabalhar com coleções no estilo “o que precisa ser feito”, e não “como exatamente fazer”. Essa abordagem é chamada de declarativa.
Mas afinal, o que é um Stream?
Não é uma coleção separada, mas sim um fluxo de dados: uma sequência de elementos que passam por uma cadeia de operações — filtragem, transformação, ordenação, coleta do resultado etc. O stream não armazena nada; ele apenas “puxa” os dados por um pipeline. As operações podem ser combinadas quase como blocos de LEGO: você monta a cadeia e executa.
Exemplo:
List<String> names = users.stream() // criamos um stream a partir da lista de usuários
.filter(u -> u.getAge() >= 18) // mantemos apenas quem já tem 18 anos
.map(User::getName) // transformamos User → String (pegamos o nome)
.collect(Collectors.toList()); // coletamos o resultado em uma lista
E pronto — em uma única linha descrevemos toda a tarefa: “pegue os usuários, filtre por idade, extraia os nomes, colete em uma lista”. Não é preciso escrever laços manualmente, criar variáveis temporárias e garantir que nada seja esquecido.
O Stream API funciona como uma linha de montagem: você define as etapas de processamento e os dados passam por elas. É bonito, compacto e muito mais fácil de ler.
3. Vantagens do Stream API
- Concisão e legibilidade. Compare as duas abordagens — imperativa e declarativa — na mesma tarefa. Fica claro qual delas é mais fácil de ler e manter.
- Composição de operações fácil. As operações são fáceis de “empilhar” em uma cadeia: filter, map, sorted, collect — tudo em uma sequência coesa.
- Menos erros. O Stream API elimina a rotina: não é preciso gerenciar coleções intermediárias e laços manualmente — menor chance de esquecer um passo ou cometer erro de indexação.
- Possibilidade de paralelismo. É fácil escalar o processamento para vários núcleos, apenas chamando parallelStream() em vez de stream(). Detalhes — mais adiante.
- Estilo de programação moderno. Ideias de programação funcional (composição de funções, ausência de laços explícitos) aumentam a expressividade e são valorizadas no mercado.
Breve histórico do Stream API
O Stream API apareceu no Java 8 (2014) e foi um salto qualitativo para a linguagem. Antes disso, qualquer operação não triviale sobre coleções exigia muito código auxiliar, enquanto outras plataformas já ofereciam abordagens declarativas (map, filter, reduce etc.).
Desde então, o Stream API é o padrão de fato para processamento de coleções em Java. Se você quer escrever código Java moderno — não dá para ficar sem ele.
4. Áreas de aplicação do Stream API
- Filtragem: selecionar apenas os elementos necessários (por exemplo, todos os usuários maiores de 18 anos).
- Transformação: extrair um campo ou criar um novo objeto (por exemplo, a lista de nomes de usuários).
- Agregação: calcular soma, média, quantidade, máximo/mínimo.
- Ordenação: ordenar os elementos por um critério desejado.
- Agrupamento: dividir os elementos por categorias.
- Coleta do resultado: coletar em lista, conjunto, mapa, string etc.
Exemplos de tarefas:
- Obter a lista de e‑mails de todos os usuários cujo nome começa com “A”.
- Calcular a idade média dos usuários.
- Encontrar o primeiro usuário com um determinado e‑mail.
- Reunir todas as cidades de residência únicas em um único conjunto.
5. Nuances úteis
Como o stream funciona
[User1] --\
[User2] ---|--> [ filter ] --> [ map ] --> [ collect ] --> List<String>
[User3] --/
1. filter — permite apenas os usuários que satisfazem a condição.
2. map — transforma o usuário em, por exemplo, e‑mail.
3. collect — coleta os e‑mails na coleção desejada.
Sintaxe: como criar um stream
- De uma coleção: list.stream() — stream dos elementos da coleção.
- De um array: Arrays.stream(array)
- De valores isolados: Stream.of("a", "b", "c")
6. Erros comuns ao migrar para o Stream API
Erro nº 1: tentar modificar a coleção dentro do stream. O Stream API não é destinado a modificar a coleção de origem (por exemplo, não faça list.remove() dentro de forEach). Para remoções use, por exemplo, removeIf na coleção antes ou depois do processamento.
Erro nº 2: confusão entre coleção e stream. Um stream não é uma coleção! O stream “percorre” os elementos uma vez e depois é fechado. Se precisar processar os dados novamente — crie um novo stream().
Erro nº 3: cadeias complexas demais. Não transforme a expressão em um “monstro” com dezenas de operações. Se a cadeia ficar longa — quebre-a em algumas etapas com variáveis intermediárias para legibilidade e depuração.
GO TO FULL VERSION