image

Bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image

MC

Mauricio Cruz13/10/2025 22:28
Compartilhe

Fundamentos de Java: Explorando o Poder das Streams

    Introdução

    Java é uma das linguagens de programação mais utilizadas no mundo, essencialmente em áreas como desenvolvimento web, aplicativos Android e sistemas empresariais.

    Criado em 1995 pela Sun Microsystems, atualmente sob domínio da Oracle, o Java destaca-se por sua portabilidade, graças à JVM e a orientação a objetos.

    Uma inovação marcante, desde o Java 8, é o pacote `java.util.stream`, que introduz Streams, permitindo manipular coleções de forma funcional e eficiente.

    Muitos iniciantes lutam com loops tradicionais para processar dados, que podem ser demorados e propensos a erros.

    Este artigo explora os fundamentos de Java com foco em Streams, abordando o conceito, exemplos práticos e sua relevância, visando ajudar desenvolvedores a entender essa ferramenta poderosa.

    O que são Streams em Java?

    Streams, parte do pacote `java.util.stream`, são sequências de elementos que suportam operações agregadas, como filtrar, mapear e reduzir, de maneira funcional.

    Diferem de coleções como `List` ou `Map` porque não armazenam dados, atuando como fluxos que processam elementos de uma fonte como listas ou arquivos sem alterá-la. Conforme a documentação do Java, Streams possuem características únicas:

    • **Sem armazenamento:** Um fluxo não é uma estrutura de dados que armazena elementos; em vez disso, ele transporta elementos de uma fonte, como uma estrutura de dados, um array, uma função geradora ou um canal de E/S, por meio de um pipeline de operações computacionais.
    • **Funcionais:** Uma operação em um fluxo produz um resultado, mas não modifica sua origem.
    • *Lazy:** Técnica de adiar a execução de uma operação (como carregar dados ou renderizar um objeto) até o momento em que o resultado é realmente necessário. Muitas operações de stream, como filtragem, mapeamento ou remoção de duplicatas, podem ser implementadas de forma preguiçosa, permitindo otimizações.
    • **Consumíveis:** Os elementos da Stream são visitados uma única vez durante o ciclo de vida da Stream.
    • **Ilimitadas:** Enquanto as Collections tem tamanho determinado (finito), as Streams podem processar fluxos infinitos.

    Essas propriedades tornam Streams ideais para tarefas como calcular totais ou filtrar dados, simplificando o código e abrindo portas para paralelismo.

    Aplicação Prática: Tutorial com Streams, List, Map, Try-with-Resources e JUnit

    Vamos aplicar Streams em cenários reais, integrando `List`, `Map`, `try-with-resources` e testes com JUnit.

    Os exemplos são baseados na documentação oficial e adaptados para iniciantes.

    Exemplo 1: Filtrando e Transformando uma Lista com Streams

    Imagine uma lista de produtos com preços. Queremos filtrar itens abaixo de 50 e convertê-los para maiúsculas.

    import java.util.List;
    import java.util.stream.Collectors;
    
    
    public class StreamExample {
      public static void main(String[] args) {
          List<String> products = List.of("Camiseta", "Calça", "Tênis", "Boné");
          List<Double> prices = List.of(30.0, 80.0, 45.0, 25.0);
    
    
          List<String> affordableProducts = products.stream()
                  .filter(p -> prices.get(products.indexOf(p)) < 50.0)
                  .map(String::toUpperCase)
                  .collect(Collectors.toList());
    
    
          System.out.println("Produtos < 50: " + affordableProducts);
          // Saída: [CAMISETA, TÊNIS, BONÉ]
      }
    }
    

    **Explicação:**

    - `stream()` inicia o fluxo.

    - `filter` seleciona produtos com preço < 50.

    - `map` chama o método toUpperCase() da classe String que transforma em maiúsculas.

    - `collect` cria uma nova lista do Tipo String, pois o retorno do método toUpperCase() da classe String, retorna uma String.

    Exemplo 2: Agrupando Dados com Map e Streams

    Agora, agrupamos produtos por categoria usando um `Map`.

    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    
    public class StreamMapExample {
      record Product(String name, String category, double price) {}
    
    
      public static void main(String[] args) {
          List<Product> products = List.of(
              new Product("Camiseta", "Roupas", 30.0),
              new Product("Calça", "Roupas", 80.0),
              new Product("Tênis", "Calçados", 45.0)
          );
    
    
          Map<String, List<Product>> byCategory = products.stream()
                  .collect(Collectors.groupingBy(Product::category));
    
    
          System.out.println("Por categoria: " + byCategory);
          //Por categoria: {Roupas=[Product[name=Camiseta, category=Roupas, price=30.0], Product[name=Calça, category=Roupas, price=80.0]], Calçados=[Product[name=Tênis, category=Calçados, price=45.0]]}
      }
    }
    

    **Explicação:**

    - `groupingBy` organiza os produtos pela propriedade category.

    - `record` simplifica a criação da classe `Product`.

    - Resultado: um `Map` com listas por categoria.

    Exemplo 3: Tratamento de Exceções com Try-with-Resources

    Streams podem ler arquivos com `try-with-resources` para garantir fechamento automático.

    import java.io.BufferedReader;
    import java.io.FileReader;
    import java.util.List;
    import java.util.stream.Collectors;
    
    
    public class StreamFileExample {
      public static void main(String[] args) {
          try (BufferedReader reader = new BufferedReader(new FileReader("products.txt"))) {
              List<String> cProducts = reader.lines()
                      .filter(line -> line.startsWith("C"))
                      .collect(Collectors.toList());
              System.out.println("Com 'C': " + cProducts);
              //Com 'C': [Camiseta, Calça]
          } catch (Exception e) {
              System.err.println("Erro: " + e.getMessage());
          }
      }
    }
    

    **Explicação:**

    - `try-with-resources` fecha o reader.

    - `lines()` cria uma Stream de linhas.

    - `filter` pega linhas com "C".

    - Supondo que o arquivo `products.txt` contem: "Camiseta", "Calça", "Tênis".

    Exemplo 4: Testando com JUnit

    Testamos o primeiro exemplo com JUnit.

    import org.junit.jupiter.api.Test;
    import java.util.List;
    import java.util.stream.Collectors;
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    
    public class StreamExampleTest {
      @Test
      void testFilter() {
          List<String> products = List.of("Camiseta", "Calça", "Tênis");
          List<Double> prices = List.of(30.0, 80.0, 45.0);
    
    
          List<String> result = products.stream()
                  .filter(p -> prices.get(products.indexOf(p)) < 50.0)
                  .map(String::toUpperCase)
                  .collect(Collectors.toList());
    
    
          assertEquals(List.of("CAMISETA", "TÊNIS"), result);
      }
    }
    //Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
    

    **Explicação:**

    - `@Test` define o teste.

    - `assertEquals` verifica o resultado.

    - Garante funcionalidade correta.

    Paralelismo em Streams

    O processamento de elementos com um for-loop explícito é inerentemente serial. Os fluxos facilitam a execução paralela, reformulando a computação como um pipeline de operações agregadas, em vez de operações imperativas em cada elemento individual. Todas as operações de fluxo podem ser executadas em série ou em paralelo. As implementações de fluxo no JDK criam fluxos seriais, a menos que o paralelismo seja explicitamente solicitado.

    Por exemplo, Collection possui os métodos Collection.stream()e Collection.parallelStream(), que produzem fluxos sequenciais e paralelos, respectivamente; outros métodos que suportam fluxos, como , IntStream.range(int, int) produzem fluxos sequenciais, mas esses fluxos podem ser paralelizados eficientemente invocando seu método BaseStream.parallel().

    Exemplo:

    int sum = products.parallelStream()
                    .filter(p -> p.price() < 50)
                    .mapToDouble(Product::price)
                    .sum();
    

    **Cuidados:**

    • **Evite interferência (não modifique a fonte):** Durante o processamento de uma Stream, é essencial que a fonte de dados (como uma lista ou coleção) não seja alterada. Modificações, como adicionar ou remover elementos enquanto a Stream é executada, podem causar comportamentos imprevisíveis, como exceções (ConcurrentModificationException) ou resultados incorretos. Isso ocorre porque Streams assumem que a fonte permanece estável. Por exemplo, se você usa parallelStream() em uma ArrayList e adiciona itens durante a execução, o resultado pode variar ou falhar. A documentação do Java 25 reforça que, exceto em coleções concorrentes (como ConcurrentHashMap), a não-interferência é obrigatória para garantir consistência, especialmente em execuções paralelas.
    • **Use lambdas stateless para segurança:** Lambdas ou funções passadas como parâmetros (ex.: em filter ou map) devem ser "stateless", ou seja, não devem depender ou modificar estado externo (como variáveis globais). Se uma lambda mantém estado (ex.: usa uma lista para rastrear valores vistos), em execuções paralelas, diferentes threads podem sobrescrever ou acessar esse estado de forma inconsistente, levando a resultados não determinísticos. Por exemplo, uma lambda que adiciona elementos a uma Set sincronizada pode funcionar, mas introduz riscos de concorrência. A solução é manter as lambdas puras, baseando-se apenas nos dados de entrada, garantindo segurança e previsibilidade, como recomendado na documentação para operações paralelas.

    Conclusão

    Streams no pacote `java.util.stream` revolucionam a manipulação de dados em Java, oferecendo uma abordagem funcional e eficiente.

    Exploramos seu conceito (lazy, funcional, paralela) e aplicamos em `List`, `Map`, `try-with-resources` e JUnit.

    Esses exemplos mostram como Streams simplificam tarefas e melhoram a performance.

    Dominar Streams é essencial para desenvolver aplicações modernas, conforme destacado na documentação do Java 25. Pratique e eleve suas habilidades!

    Mão na massa

    Teste os exemplos em uma IDE como IntelliJ! ou VSCode. Crie suas próprias Streams e compartilhe nos comentários ou em fóruns. Se gostou, vote e compartilhe com outros aprendizes de Java!

    Referências

    - Java SE 25 Documentation: java.util.stream Package Summary. Oracle, 2025. Disponível em: <https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/stream/package-summary.html>.

    Compartilhe
    Recomendados para você
    Cognizant - Mobile Developer
    Luizalabs - Back-end com Python
    PcD Tech Bradesco - Java & QA Developer
    Comentários (0)