Adeus, for Loops! Como a Stream API do Java Deixa seu Código Mais Limpo e Poderoso
Introdução
Escrever loops for aninhados para filtrar e transformar listas em Java é uma prática comum, mas nem sempre a mais eficiente ou legível. A Stream API oferece uma alternativa mais declarativa, permitindo que operações complexas sejam realizadas de forma concisa e elegante, muitas vezes em uma única linha de código.
O Paradigma por Trás da Stream API em Java: O que são Streams?
- O que é um stream? Stream é uma sequência de elementos que suporta operações de agregação.
- O que é operação de agregação? Agrupar informações e transformá-las em uma só.
Stream não armazena dados! Stream, em uma tradução literal como fluxo, é um processo de dados em sequência, onde cada etapa transforma, reduz ou filtra os elementos de uma fonte (como Arrays, Collections, etc.).
Possui funções intermediárias e terminais, mas o que é isso exatamente?
- As funções intermediárias são aquelas que transformam, filtram ou organizam os dados, sem encerrar o fluxo. Elas retornam um novo stream, permitindo o encadeamento de várias operações.
- As funções terminais são aquelas que finalizam o fluxo e produzem um resultado concreto, encerrando o processo.
Explorando o Poder da Stream API de Java: Operações Essenciais
Funções Intermediárias
- filter() - Serve para filtrar elementos com base em uma condição. Somente os elementos que satisfazem a expressão booleana passada permanecem no stream.
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> pares = numeros.stream()
.filter(n -> n % 2 == 0) // Deixa apenas números pares
.toList();
System.out.println(pares); // [2, 4, 6]
- map() - É usada para transformar cada elemento do stream em outro tipo de valor ou formato.
List<String> nomes = Arrays.asList("diego", "ana", "paulo");
List<String> nomesMaiusculos = nomes.stream()
.map(String::toUpperCase) // Transforma cada nome em maiúsculo
.toList();
System.out.println(nomesMaiusculos); // [DIEGO, ANA, PAULO]
Outras Funções Intermediárias Importantes
- distinct() - Remove elementos duplicados.
- sorted() - Ordena os elementos (alfabeticamente ou numericamente).
- limit(n) - Pega apenas os primeiros n elementos.
- skip(n) - Ignora os primeiros n elementos.
- peek() - Usada para “espiar” o conteúdo durante o processamento, geralmente para depuração.
List<String> lista = Arrays.asList("Diego", "Ana", "Ana", "Paulo", "Lucas", "Luiza");
List<String> resultado = lista.stream()
.filter(nome -> nome.length() > 3) // Deixa apenas nomes com mais de 3 letras
.distinct() // Remove duplicados
.sorted() // Ordena alfabeticamente
.peek(System.out::println) // Mostra cada nome durante o fluxo
.limit(4) // Limita aos 4 primeiros resultados
.toList();
System.out.println("Resultado final: " + resultado);
Funções Terminais
- collect() - Coleta os elementos do stream e os transforma em uma coleção (como List, Set) ou outro tipo de resultado.
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> pares = numeros.stream()
.filter(n -> n % 2 == 0) // mantém apenas pares
.collect(Collectors.toList()); // coleta em uma lista
System.out.println(pares); // [2, 4, 6]
- forEach() - Executa uma ação para cada elemento do stream. Muito útil para imprimir ou aplicar alguma operação.
List<String> nomes = Arrays.asList("Diego", "Ana", "Paulo");
nomes.stream()
.forEach(System.out::println);
// Saída:
// Diego
// Ana
// Paulo
- count() - Retorna a quantidade de elementos que passaram pelo stream, útil para estatísticas.
long quantidadePares = numeros.stream()
.filter(n -> n % 2 == 0)
.count();
System.out.println("Quantidade de pares: " + quantidadePares); // 3
- reduce() - Reduz todos os elementos do stream a um único valor, aplicando uma operação cumulativa, como soma, multiplicação ou concatenação.
int soma = numeros.stream()
.reduce(0, (a, b) -> a + b); // soma todos os números
System.out.println("Soma total: " + soma); // 21
- anyMatch(), allMatch(), noneMatch() - Verificam se algum, todos ou nenhum elemento atende a uma condição.
boolean temPar = numeros.stream().anyMatch(n -> n % 2 == 0);
System.out.println("Tem algum número par? " + temPar); // true
- findFirst() / findAny() - Retornam o primeiro ou qualquer elemento do stream que corresponda a um critério. Retornam um Optional.
Optional<Integer> primeiroPar = numeros.stream()
.filter(n -> n % 2 == 0)
.findFirst();
System.out.println("Primeiro par: " + primeiroPar.orElse(-1)); // 2
Boas Práticas e Erros Comuns na Java Stream API
- Evitar reusar a mesma stream: Streams em Java são consumidos por operações terminais. Tentar reutilizar o mesmo stream gera IllegalStateException, pois o fluxo de dados já foi “fechado”.
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
- Preferir operações intermediárias stateless quando possível: Operações stateless não mantêm estado entre os elementos do fluxo.
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6);
numeros.stream()
.filter(n -> n % 2 == 0) // stateless
.map(n -> n * 2) // stateless
.forEach(System.out::println);
Em streams grandes, operações stateful podem ser lentas ou causar uso excessivo de memória. Operações stateless permitem paralelização mais eficiente.
Conclusão
Como vimos, a Stream API do Java é mais do que uma ferramenta: é uma mudança de paradigma que promove um código mais limpo e funcional. Ao dominar operações como map e filter, você escreve menos e expressa mais.
Participe
Agora é sua vez! Adicione seus conhecimentos ao repositório do projeto: https://github.com/diegooilv/stream-api-java! Veja a pasta exemplos/ e adicione um exemplo seu!
Referências
- Documentação Oficial do Java Stream: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
- Tutorial (Baeldung): https://www.baeldung.com/java-8-streams
- Repositório com o Código: https://github.com/diegooilv/stream-api-java