Article image
Swellington Soares
Swellington Soares07/06/2025 15:26
Compartilhe

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.

    Referências

    Compartilhe
    Comentários (0)