Tudo o que Você Precisa Saber Sobre Java Streams
Desde o Java 8, a API de Streams transformou a maneira como lidamos com coleções. Se antes precisávamos escrever loops verbosos e manter variáveis intermediárias para filtrar, transformar ou agrupar dados, hoje podemos fazer isso de forma mais fluente e expressiva. Mas como toda ferramenta poderosa, Streams têm suas boas práticas, armadilhas e contextos ideais. Neste artigo, vamos destrinchar essa API: dos fundamentos até os casos avançados.
O que é uma Stream?
Uma Stream é uma abstração para processar sequências de dados de forma funcional. Ela permite:
- Filtrar elementos (filter)
- Transformá-los (map)
- Ordenar (sorted)
- Agrupar (groupingBy)
- Reduzir a um valor único (reduce)
- E muito mais...
Observer que Streams não armazenam dados. Elas operam sobre fontes de dados (como listas, arrays ou arquivos) e produzem resultados com avaliação preguiçosa (lazy) — ou seja, só executam quando necessário.
Como funciona a arquitetura de uma Stream?
A arquitetura pode ser dividida em 3 partes:
1. Fonte de Dados (Data Source)
Pode ser uma Collection, array, I/O channel, ou qualquer estrutura de dados que possa ser convertida em stream.
Exemplo: List<String> nomes = List.of("Ana", "João", "Pedro");
2. Operações Intermediárias
São operações que transformam uma Stream em outra Stream.
São lazy (preguiçosas): não são executadas até que uma operação terminal seja invocada.
Exemplos: map, filter, distinct, sorted, peek.
nomes.stream()
.filter(nome -> nome.startsWith("A"))
.map(String::toUpperCase);
3. Operação Terminal
Finaliza o pipeline e produz um resultado.
Após ser chamada, a stream não pode mais ser usada.
Exemplos: collect, forEach, count, reduce, anyMatch.
long count = nomes.stream()
.filter(nome -> nome.startsWith("A"))
.count();
Principais Operações
.filter(predicate): Filtra elementos com base em uma condição.
//Exemplo
.stream().filter(s -> s.length() > 3)
.map(Function): Transforma elementos de um tipo em outro.
//Exemplo
.stream().map(String::length)
.collect(Collector): Converte a Stream em outra estrutura (lista, conjunto, mapa, string).
.collect(Collectors.toList())
.collect(Collectors.joining(", "))
.sorted(), .distinct(), .limit(), .skip(): Operações utilitárias
.stream().sorted() // ordena
.stream().distinct() //sem elementos iguais
.stream().limit(10) //limita a 10 itens
.reduce(): Reduz a stream a um único valor.
Optional<Integer> soma = list.stream().reduce((a, b) -> a + b);
.flatMap(): "Achata" múltiplos elementos em uma única stream.
List<String> palavras = Arrays.asList("olá mundo", "java stream");
List<String> resultado = palavras.stream()
.flatMap(p -> Arrays.stream(p.split(" ")))
.collect(Collectors.toList());
// Resultado: [olá, mundo, java, stream]
Streams em Coleções Complexas
Imagine uma lista de objetos Pessoa:
class Pessoa {
String nome;
int idade;
String cidade;
// getters e construtor
}
//Mock
List<Pessoa> pessoas = new ArrayList<>()
pessoas.add(new Pessoa("Ana", 25, "São Paulo"));
pessoas.add(new Pessoa("Bruno", 30, "Rio de Janeiro"));
pessoas.add(new Pessoa("Carla", 22, "Rio de Janeiro"));
pessoas.add(new Pessoa("Diego", 28, "Curitiba"));
Algumas operações:
//FILTRAR E AGRUPAR POR CIDADE
Map<String, List<Pessoa>> porCidade = pessoas.stream()
.filter(p -> p.idade > 18)
.collect(Collectors.groupingBy(Pessoa::getCidade));
//{Curitiba=[Diego], São Paulo=[Ana], Rio de Janeiro=[Bruno, Carla]}
Map<String, Double> mediaIdade = pessoas.stream()
.collect(Collectors.groupingBy(
Pessoa::getCidade,
Collectors.averagingInt(Pessoa::getIdade)
));
//{Curitiba=28.0, São Paulo=25.0, Rio de Janeiro=26.0}
Streams Paralelas
Quer processar grandes volumes de dados em paralelo? Use:
list.parallelStream()
Isso divide os dados em múltiplas threads. Mas atenção:
- Só vale a pena para conjuntos grandes e operações pesadas.
- Evite efeitos colaterais e recursos não thread-safe.
- Pode até ser mais lento que stream() dependendo do caso.
Quando Usar Streams
- Você está manipulando coleções (filtros, mapeamentos, ordenações).
- Deseja código conciso e legível.
- Prefere imutabilidade e programação funcional.
- Trabalha com operações em lote, especialmente em pipelines de dados.
Quando Evitar Streams
- Você precisa de controle preciso de fluxo (ex: break, continue).
- Há efeitos colaterais intensos no processamento (ex: modificando o estado de objetos externos).
- O código precisa de debug linha a linha.
- A performance crítica depende de loops altamente otimizados.
Armadilhas Comuns
Ignorar o terminal
nomes.stream().filter(n -> n.length() > 3); // não faz nada!
Achar que forEach é sempre melhor
forEach() é útil, mas geralmente é o último recurso. Prefira collect() ou map().
Misturar Streams com mutabilidade
List<String> resultado = new ArrayList<>();
list.stream().forEach(resultado::add); // Evite! Prefira collect()
Flatten errado com map() em vez de flatMap()
list.stream().map(s -> s.split(" ")); // Stream<String[]>
list.stream().flatMap(s -> Arrays.stream(s.split(" "))); // Stream<String>
Dicas Avançadas
Combine peek() com logs para depuração leve:
.peek(e -> System.out.println("Filtrando: " + e))
Use Collectors.partitioningBy() para separar em dois grupos:
Map<Boolean, List<Pessoa>> maioresDeIdade = pessoas.stream()
.collect(Collectors.partitioningBy(p -> p.getIdade() >= 18));
Use Optional com Streams:
Optional<String> primeiroComA = nomes.stream()
.filter(n -> n.startsWith("A"))
.findFirst();
Conclusão
A API de Streams no Java é uma adição poderosa, moderna e elegante ao ecossistema da linguagem. Ela permite escrever código conciso e declarativo, reduzindo o ruído de loops verbosos. No entanto, como qualquer ferramenta, seu uso exige equilíbrio e contexto. Streams não são substitutos de todos os fors e ifs, e abusar de pipelines gigantescos pode sacrificar a clareza.