image

Accede a bootcamps ilimitados y a más de 650 cursos para siempre

60
%OFF
Article image

LF

Lucimar Fernandes23/10/2025 08:31
Compartir

🏆 Java: Da Base à Stream API com Boas Práticas e Exemplos Reais

    Java já foi sinônimo de for, while e aquele ritual quase religioso de iterar listas com cuidado. Hoje, Java também é sinônimo de fluidez, de pipelines elegantes e de um jeito mais moderno de pensar transformação de dados. A Stream API não apareceu por moda. Ela chegou para resolver problemas práticos de legibilidade, paralelismo e composição, permitindo que você escreva menos código e comunique melhor a intenção do que o seu programa deve fazer.

    Imagine que você tem uma cozinha cheia de ingredientes e precisa preparar vários pratos ao mesmo tempo. Usar loops imperativos é como cozinhar com uma panela só, cuidando de cada passo manualmente. Usar Streams é como organizar esta cozinha em estações, cada uma com uma função clara, e deixar que os ingredientes passem por um fluxo até virar o prato pronto. O exemplo é simples, mas revela a grande vantagem: código mais claro, menos chance de erro e mais facilidade para testar e evoluir.

    Neste artigo vamos entender o que é uma Stream, como montar pipelines que façam sentido, quando optar por paralelismo e quais armadilhas evitar. Haverá muitos exemplos práticos, comparações com abordagens tradicionais e dicas que você pode aplicar já no próximo refactor. Prometo explicações diretas, trechos de código que funcionam na vida real e uma linguagem que conversa com quem programa sem perder um toque humano.

    image

    Afinal, o que é o Java Stream API?

    A Stream API é uma das maiores revoluções que o Java já recebeu. Introduzida na versão 8, ela trouxe um jeito novo de lidar com coleções de dados — mais expressivo, conciso e funcional.

    Mas antes de sair usando, é importante entender o que ela realmente é: uma abstração para processar dados de forma declarativa. Em vez de dizer “como fazer”, você diz “o que fazer”, e o Java cuida do resto.

    Pense nas Streams como uma linha de montagem: os dados entram de um lado, passam por transformações, filtros e ordenações, e no fim viram o resultado que você queria; Tudo isso sem precisar controlar cada passo manualmente.

    🧩 Diferença entre Collection e Stream

    Uma Collection armazena dados.

    Uma Stream processa esses dados.

    Veja a diferença na prática:

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    
    public class ExemploStream {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("Lucimar", "Carlos", "Ana", "João");
    
    
          // Criando uma Stream a partir de uma lista
          Stream<String> stream = nomes.stream();
    
    
          // Aplicando uma operação: exibir nomes em letras maiúsculas
          stream
              .map(String::toUpperCase)
              .forEach(System.out::println);
      }
    }
    
    
    

    💬 O que aconteceu aqui:

    A lista nomes é apenas um conjunto de dados estáticos.

    Quando chamamos nomes.stream(), criamos um fluxo desses dados.

    O método .map(String::toUpperCase) transforma cada nome.

    O .forEach(System.out::println) é a operação terminal - imprime o resultado.

    Repare que não usamos nenhum for nem criamos listas auxiliares. É direto, limpo e legível.

    🔍 Comparando com o jeito “antigo”

    Antes da Stream API, o mesmo código ficaria assim:

    import java.util.Arrays;
    import java.util.List;
    
    
    public class ExemploAntigo {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("Lucimar", "Carlos", "Ana", "João");
    
    
          for (String nome : nomes) {
              System.out.println(nome.toUpperCase());
          }
      }
    }
    
    
    

    Funciona, mas à medida que o código cresce e as operações ficam mais complexas (filtrar, mapear, ordenar, somar etc.), ele começa a perder clareza e aumentar a chance de erros.

    🧠 O poder do encadeamento

    A grande sacada da Stream API está no encadeamento de operações.

    Cada chamada cria um novo fluxo, transformando o resultado da anterior — até que uma operação terminal finalize o processo.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    
    public class ExemploEncadeamento {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("Lucimar", "Carlos", "Ana", "João", "Amanda");
    
    
          List<String> resultado = nomes.stream()
              .filter(nome -> nome.startsWith("A"))    // Filtra apenas nomes que começam com A
              .map(String::toUpperCase)                // Transforma em maiúsculas
              .sorted()                                // Ordena alfabeticamente
              .collect(Collectors.toList());           // Coleta o resultado em uma nova lista
    
    
          System.out.println(resultado);
      }
    }
    
    
    

    🧩 Saída:

    [AMANDA, ANA]

    Esse exemplo mostra a beleza da Stream API:

    cada linha tem uma função clara, como se fosse uma conversa entre você e o código.

    Não há ruído, apenas intenção.

    Como funciona uma Stream na prática

    Para entender de verdade a Stream API, é preciso visualizar o seu “caminho”.

    Uma Stream é formada por três etapas principais:

    Criação da Stream – de onde os dados vêm

    Operações intermediárias – o que acontece com esses dados

    Operações terminais – o que você faz com o resultado

    Vamos ver isso fluindo em código.

    🌱 1. Criação da Stream

    O primeiro passo é criar o fluxo. Você pode gerar uma Stream a partir de várias fontes — listas, arrays, arquivos ou até valores diretos.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    
    public class CriacaoStream {
      public static void main(String[] args) {
          // A partir de uma lista
          List<String> nomes = Arrays.asList("Lucimar", "Lucas", "Fernanda", "João");
          Stream<String> streamLista = nomes.stream();
    
    
          // A partir de valores diretos
          Stream<String> streamDireta = Stream.of("Java", "Python", "C#", "Go");
    
    
          // A partir de um array
          String[] linguagens = {"Kotlin", "Swift", "Ruby"};
          Stream<String> streamArray = Arrays.stream(linguagens);
    
    
          streamDireta.forEach(System.out::println);
      }
    }
    

    🔎 Dica: Uma Stream não armazena dados, ela apenas lê e processa.

    Assim que uma operação terminal é chamada, ela não pode ser reutilizada.

    🔄 2. Operações intermediárias

    As operações intermediárias são as transformações do fluxo.

    Elas não executam nada imediatamente, apenas constroem um “pipeline” de ações que será processado quando o resultado for realmente necessário.

    Veja um exemplo prático:

    import java.util.Arrays;
    import java.util.List;
    
    
    public class IntermediariasStream {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("Lucimar", "Lucas", "Fernanda", "João", "Lara");
    
    
          nomes.stream()
              .filter(nome -> nome.length() > 4)     // Mantém apenas nomes com mais de 4 letras
              .map(String::toUpperCase)              // Converte para maiúsculas
              .sorted()                              // Ordena alfabeticamente
              .forEach(System.out::println);         // Exibe o resultado
      }
    }
    

    🧩 Saída:

    LUCIMAR

    FERNANDA

    LUCAS

    🎯 3. Operações terminais

    As operações terminais são as que encerram o fluxo.

    É nesse momento que o Java realmente executa tudo o que foi definido.

    Algumas das mais usadas são:

    forEach() – percorre os elementos

    collect() – reúne o resultado em uma nova coleção

    count() – conta quantos elementos restaram

    findFirst() – retorna o primeiro resultado encontrado

    Veja o exemplo completo:

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    
    public class OperacoesTerminais {
      public static void main(String[] args) {
          List<Integer> numeros = Arrays.asList(2, 4, 6, 8, 10, 12);
    
    
          // Coletando os números pares multiplicados por 2
          List<Integer> resultado = numeros.stream()
              .filter(n -> n % 2 == 0)
              .map(n -> n * 2)
              .collect(Collectors.toList());
    
    
          System.out.println("Resultado: " + resultado);
    
    
          // Contando quantos números passaram pelo filtro
          long quantidade = numeros.stream()
              .filter(n -> n > 5)
              .count();
    
    
          System.out.println("Números maiores que 5: " + quantidade);
      }
    }
    
    
    

    🧮 Saída:

    Resultado: [4, 8, 12, 16, 20, 24]

    Números maiores que 5: 4

    image

    Principais métodos e usos práticos

    Agora que já entendemos como as Streams funcionam, é hora de colocar a mão na massa.

    A força da Stream API está nos seus métodos expressivos — pequenos, mas poderosos.

    Eles permitem transformar, filtrar e combinar dados de formas que, antes, exigiriam dezenas de linhas de código.

    A seguir, vamos conhecer os métodos mais usados, com exemplos práticos e comentados.

    🔍 1. filter() — filtrando dados

    O método filter() serve para eliminar elementos que não atendem a uma condição.

    Ele recebe uma lambda expression que retorna true ou false.

    import java.util.Arrays;
    import java.util.List;
    
    
    public class ExemploFilter {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("Lucimar", "Carlos", "Ana", "Amanda", "João");
    
    
          nomes.stream()
              .filter(nome -> nome.startsWith("A")) // mantém apenas nomes que começam com A
              .forEach(System.out::println);
      }
    }
    
    
    

    🧩 Saída:

    Ana

    Amanda

    💡 Quando usar:

    Sempre que precisar filtrar listas, seja de produtos, clientes ou qualquer outro tipo de dado.

    ✨ 2. map() — transformando elementos

    O map() é usado para transformar os elementos da Stream.

    Ele aplica uma função a cada item e retorna um novo valor.

    import java.util.Arrays;
    import java.util.List;
    
    
    public class ExemploMap {
    public static void main(String[] args) {
      List<String> nomes = Arrays.asList("lucimar", "lucas", "joão");
    
    
      nomes.stream()
        .map(String::toUpperCase) // converte para maiúsculas
        .forEach(System.out::println);
    }
    }
    
    
    

    🧩 Saída:

    LUCIMAR

    LUCAS

    JOÃO

    Explicando:

    Se filter() é um “porteiro” que decide quem entra, o map() é o “estilista” que transforma quem passou pela porta.

    🔢 3. sorted() — ordenando elementos

    O sorted() organiza os dados da Stream.

    Por padrão, ele usa a ordem natural dos elementos (alfabética, numérica etc.), mas também pode receber um comparador personalizado.

    import java.util.Arrays;
    import java.util.List;
    
    
    public class ExemploSorted {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("João", "Carlos", "Lucimar", "Ana");
    
    
          System.out.println("Ordem alfabética:");
          nomes.stream()
              .sorted()
              .forEach(System.out::println);
    
    
          System.out.println("\nOrdem inversa:");
          nomes.stream()
              .sorted((a, b) -> b.compareTo(a)) // comparador reverso
              .forEach(System.out::println);
      }
    }
    
    
    

    🧩 Saída:

    Ordem alfabética:

    Ana

    Carlos

    Lucimar

    João

    Ordem inversa:

    João

    Lucimar

    Carlos

    Ana

    🧺 4. collect() — reunindo resultados

    O collect() é o método que “fecha” a Stream e reúne os dados processados em uma nova coleção (geralmente uma List ou Set).

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    
    public class ExemploCollect {
      public static void main(String[] args) {
          List<String> nomes = Arrays.asList("Lucimar", "Lucas", "Amanda", "Ana");
    
    
          List<String> nomesComA = nomes.stream()
              .filter(nome -> nome.startsWith("A"))
              .collect(Collectors.toList());
    
    
          System.out.println(nomesComA);
      }
    }
    

    🧩 Saída:

    [Amanda, Ana]

    💬 Resumo:

    Se a Stream é um rio, o collect() é a represa — ele guarda tudo o que passou por ali.

    🧠 5. Exemplo completo com objetos: “Funcionários e salários”

    Vamos ver um exemplo mais “de mundo real”.

    Imagine uma lista de funcionários com nome e salário. Queremos saber:

    Quem ganha acima de R$ 3.000

    Seus nomes em letras maiúsculas

    Ordenados alfabeticamente

    import java.util.*;
    import java.util.stream.Collectors;
    
    
    class Funcionario {
      String nome;
      double salario;
    
    
      Funcionario(String nome, double salario) {
          this.nome = nome;
          this.salario = salario;
      }
    
    
      public String getNome() {
          return nome;
      }
    
    
      public double getSalario() {
          return salario;
      }
    }
    
    
    public class ExemploFuncionarios {
      public static void main(String[] args) {
          List<Funcionario> funcionarios = Arrays.asList(
              new Funcionario("Lucimar", 4500),
              new Funcionario("João", 2800),
              new Funcionario("Lara", 3700),
              new Funcionario("Carlos", 2200),
              new Funcionario("Ana", 5000)
          );
    
    
          List<String> nomesAltosSalarios = funcionarios.stream()
              .filter(f -> f.getSalario() > 3000)      // filtra salários maiores que 3000
              .map(f -> f.getNome().toUpperCase())     // transforma nome em maiúsculas
              .sorted()                                // ordena alfabeticamente
              .collect(Collectors.toList());           // coleta em uma nova lista
    
    
          System.out.println("Funcionários com salário acima de 3000:");
          nomesAltosSalarios.forEach(System.out::println);
      }
    }
    

    🧩 Saída:

    Funcionários com salário acima de 3000:

    ANA

    LUCIMAR

    LARA

    💬 Perfeito exemplo prático:

    Poucas linhas, nenhum laço explícito e o código transmite exatamente a intenção: filtrar, transformar e ordenar.

    Boas práticas no uso da Stream API

    A Stream API é poderosa, mas com grande poder vem grande responsabilidade.

    Usar Streams de maneira inteligente faz diferença entre código claro, eficiente e fácil de manter ou um fluxo confuso, difícil de depurar e pouco performático.

    Vamos ver algumas boas práticas essenciais:

    1. Prefira encadeamentos curtos e claros

    Evite criar pipelines enormes de dezenas de operações.

    O ideal é manter cada fluxo compreensível, facilitando a leitura e a manutenção do código.

    // Pipeline claro e direto
    List<String> nomes = Arrays.asList("Lucimar", "Lucas", "Ana", "João");
    List<String> resultado = nomes.stream()
      .filter(n -> n.startsWith("A"))
      .map(String::toUpperCase)
      .collect(Collectors.toList());
    
    
    System.out.println(resultado);
    

    💡 Dica: Se o fluxo ficar muito grande, quebre em métodos auxiliares.

    2. Evite modificar coleções dentro de Streams

    Streams não devem alterar a coleção original.

    Alterar listas dentro do fluxo pode gerar comportamento inesperado ou exceções.

    ❌ Exemplo errado:

    nomes.stream()
    .forEach(n -> nomes.remove(n)); // Pode lançar ConcurrentModificationException
    

    ✅ Correção:

    List<String> filtrados = nomes.stream()
    .filter(n -> !n.equals("Ana"))
    .collect(Collectors.toList());
    

    3. Use paralelismo com cuidado

    O método parallelStream() divide o processamento entre múltiplos núcleos da CPU, acelerando operações grandes.

    Mas nem sempre é vantajoso. Operações pequenas ou dependentes de ordem podem diminuir a performance ou gerar resultados imprevisíveis.

    List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    
    int soma = numeros.parallelStream()
    .mapToInt(Integer::intValue)
    .sum();
    
    System.out.println("Soma: " + soma);
    

    💡 Dica: Teste sempre a performance antes de adotar paralelismo.

    4. Prefira expressividade à complexidade

    O objetivo da Stream API é tornar o código mais declarativo.

    Evite criar lógicas complexas dentro de uma única operação. Divida em métodos ou crie variáveis intermediárias para clareza.

    // Bom exemplo
    List<String> nomesMaiores = nomes.stream()
    .filter(Lucimar::nomeValido) // método externo melhora a leitura
    .collect(Collectors.toList());
    

    5. Evite operações terminais múltiplas em uma mesma Stream

    Uma Stream só pode ter uma operação terminal. Chamá-la mais de uma vez gera exceção ou comportamento inesperado.

    ❌ Errado:

    Stream<String> stream = nomes.stream();
    stream.forEach(System.out::println);
    stream.count(); // Exception!
    

    ✅ Correto:

    long quantidade = nomes.stream().count();
    

    6. Prefira métodos de conveniência do Collectors

    O Collectors oferece métodos prontos para criar listas, sets, mapas ou até agrupar dados.

    Evite criar coleções manualmente quando o collect() já faz isso de forma elegante.

    Map<Character, List<String>> agrupados = nomes.stream()
    .collect(Collectors.groupingBy(n -> n.charAt(0)));
    
    System.out.println(agrupados);
    

    💬 Saída típica:

    {A=[Ana, Amanda], L=[Lucas], J=[João]}

    Conclusão

    Ao longo deste artigo, exploramos o universo do Java, desde os primeiros passos e boas práticas, passando por orientação a objetos, até chegar à poderosa Stream API, que transforma a maneira como lidamos com coleções e dados. Cada subtema foi pensado para fornecer não apenas conhecimento teórico, mas ferramentas práticas que você pode aplicar no seu dia a dia como desenvolvedor.

    🌟 Principais aprendizados

    Primeiros passos e boas práticas:

    Começar com boas práticas é essencial. Declarar variáveis de forma clara, manter convenções de nomes, tratar exceções e organizar o código desde o início evita erros futuros e facilita a manutenção.

    Orientação a objetos:

    Entender conceitos como classes, objetos, herança, encapsulamento e polimorfismo não é apenas obrigatório, mas é a base para escrever sistemas robustos, escaláveis e legíveis. Aplicar esses conceitos torna o código mais modular e próximo da forma como pensamos no mundo real.

    Erros comuns de iniciantes:

    Conhecer os erros típicos — como reutilizar Streams, modificar coleções durante o processamento ou criar pipelines complexos demais — ajuda a evitá-los. Saber antecipar essas armadilhas economiza tempo e reduz frustrações.

    Java Stream API:

    A Stream API é um divisor de águas. Ela permite processar dados de forma declarativa, legível e eficiente, seja filtrando, transformando, agregando ou coletando resultados. Quando combinada com boas práticas, ela simplifica operações complexas que, de outra forma, exigiriam múltiplos loops e listas temporárias.

    Boas práticas com Streams:

    Pipelines curtos e claros, operações imutáveis, paralelismo consciente e o uso inteligente dos métodos do Collectors são fundamentais. Aplicando essas práticas, o código se torna mais robusto, eficiente e elegante.

    Java não é apenas uma linguagem; é uma filosofia de robustez, clareza e escalabilidade.

    A Stream API, em particular, nos ensina a pensar em dados como fluxos, e não como sequências de passos manuais. Isso muda a forma como escrevemos código: menos loops, menos complexidade, mais legibilidade e elegância.

    O desenvolvedor que domina essas técnicas não só escreve código que funciona, mas escreve código que comunica sua intenção. E isso é o que separa um programador que apenas codifica de um que constrói software de verdade.

    Referências:

    https://www.linkedin.com/pulse/unlocking-success-java-stream-api-gurunath-kadam-frwmf/

    https://www.devmedia.com.br/java-streams-api-manipulando-colecoes-de-forma-eficiente/37630

    https://medium.com/@fabio.alvaro/java-streams-uso-de-streams-api-introduzidas-no-java-8-para-opera%C3%A7%C3%B5es-funcionais-0d920c26e3c5

    https://deviniciative.wordpress.com/2021/08/23/entendendo-o-java-stream-api/

    Compartir
    Recomendado para ti
    Cognizant - Mobile Developer
    Luizalabs - Back-end com Python
    PcD Tech Bradesco - Java & QA Developer
    Comentarios (2)
    José Lucas
    José Lucas - 23/10/2025 12:51

    incrível parabéns Lucimar

    DIO Community
    DIO Community - 23/10/2025 08:48

    Excelente, Lucimar! Que artigo incrível e super completo sobre Java Stream API! É fascinante ver como você aborda essa funcionalidade, mostrando que o Stream API revolucionou a forma de manipular coleções, transformando o código imperativo em um código funcional, elegante e eficiente.

    Você demonstrou que o Stream API é mais que uma ferramenta, sendo um novo jeito de pensar em como lidar com dados, é o ponto crucial. O maior desafio para um desenvolvedor ao trabalhar com um projeto que usa o padrão MVC (Model-View-Controller), em termos de manter a separação de responsabilidades e de evitar o acoplamento entre as três camadas, em vez de apenas focar em fazer a aplicação funcionar, é a capacidade de impedir que a lógica de negócios vaze para as camadas de View ou Controller.

    Qual você diria que é o maior desafio para um desenvolvedor ao trabalhar com um projeto que usa o padrão MVC, em termos de manter a separação de responsabilidades e de evitar o acoplamento entre as três camadas, em vez de apenas focar em fazer a aplicação funcionar?