Skip to the content.

API de Streams do Java

Este documento oferece uma série de aulas sobre a API de Streams do Java, introduzida no Java 8. As Streams revolucionaram a forma como coleções de dados são processadas, permitindo um estilo de programação mais funcional, declarativo e expressivo.


Aula 1: Introdução à API de Streams

Nesta primeira aula, vamos entender o que são Streams, seus conceitos fundamentais e as vantagens de utilizá-las.

O que é uma Stream?

Uma Stream é uma sequência de elementos provenientes de uma fonte de dados que suporta operações de agregação. É importante frisar que uma Stream não é uma estrutura de dados, mas sim um fluxo de dados que pode ser processado.

Características Principais:

Vantagens de usar Streams

Criando uma Stream

Existem várias maneiras de se obter uma Stream. As mais comuns são:


Aula 2: Operações Intermediárias

As operações intermediárias transformam uma Stream em outra. Elas são sempre “lazy” e são encadeadas para formar um pipeline.

Principais Operações Intermediárias


Aula 3: Operações Terminais

As operações terminais produzem um resultado ou um “side-effect”. Uma vez que uma operação terminal é executada, a Stream é consumida e não pode ser reutilizada.

Principais Operações Terminais


Aula 4: Exemplos do Mundo Real e Casos de Uso Complexos

Vamos aplicar o que aprendemos em cenários mais práticos.

Exemplo 1: Processando uma Lista de Funcionários

class Funcionario {
    private String nome;
    private double salario;
    private String departamento;

    // construtores, getters e setters
}

List<Funcionario> funcionarios = ...; // Inicializa a lista

// Encontrar os 3 funcionários com os maiores salários do departamento de "TI"
List<Funcionario> top3TI = funcionarios.stream()
    .filter(f -> "TI".equals(f.getDepartamento()))
    .sorted(Comparator.comparingDouble(Funcionario::getSalario).reversed())
    .limit(3)
    .collect(Collectors.toList());

// Calcular o salário médio de todos os funcionários
double mediaSalarial = funcionarios.stream()
    .mapToDouble(Funcionario::getSalario)
    .average()
    .orElse(0.0);

// Agrupar funcionários por departamento
Map<String, List<Funcionario>> porDepartamento = funcionarios.stream()
    .collect(Collectors.groupingBy(Funcionario::getDepartamento));

Exemplo 2: Análise de Dados de Log

Imagine um arquivo de log com linhas no formato [NÍVEL] MENSAGEM.

List<String> linhasLog = ...; // Lê as linhas do arquivo

// Contar o número de erros
long numeroDeErros = linhasLog.stream()
    .filter(linha -> linha.startsWith("[ERRO]"))
    .count();

// Extrair as mensagens de erro
List<String> mensagensDeErro = linhasLog.stream()
    .filter(linha -> linha.startsWith("[ERRO]"))
    .map(linha -> linha.substring(linha.indexOf("]") + 2))
    .collect(Collectors.toList());

Aula 5: Streams Paralelas e Considerações de Desempenho

As Streams podem ser processadas em paralelo para aproveitar o hardware de múltiplos núcleos.

Streams Paralelas

A conversão de uma Stream sequencial para paralela é simples:

List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Stream Sequencial
numeros.stream().forEach(System.out::print); // Ordem garantida

System.out.println();

// Stream Paralela
numeros.parallelStream().forEach(System.out::print); // A ordem não é garantida

Quando usar Streams Paralelas?

Quando evitar Streams Paralelas?

Operações com e sem estado (Stateful vs. Stateless)

Considerações de Desempenho


Aula 6: Coletores (Collectors) Avançados e Personalizados

A classe Collectors oferece uma ampla gama de funcionalidades para coletar os resultados de uma Stream.

Coletores Avançados

Criando um Coletor Personalizado

Para cenários muito específicos, você pode criar seu próprio Collector implementando a interface java.util.stream.Collector. Um Collector é definido por quatro funções:

  1. supplier(): Cria um novo contêiner de resultado mutável.
  2. accumulator(): Incorpora um novo elemento de dados em um contêiner de resultado.
  3. combiner(): Combina dois contêineres de resultado em um. Essencial para Streams paralelas.
  4. finisher(): Realiza uma transformação final opcional no contêiner de resultado.

Exemplo: Um coletor que cria uma String de elementos separados por vírgula e entre colchetes.

import java.util.StringJoiner;
import java.util.stream.Collector;

public class StringCollector {
    public static Collector<CharSequence, StringJoiner, String> toStringCollector() {
        return Collector.of(
                () -> new StringJoiner(", ", "[", "]"), // supplier
                StringJoiner::add,                      // accumulator
                StringJoiner::merge,                    // combiner
                StringJoiner::toString                  // finisher
        );
    }
}

// Uso
List<String> itens = Arrays.asList("Maçã", "Banana", "Laranja");
String resultado = itens.stream().collect(StringCollector.toStringCollector());
System.out.println(resultado); // [Maçã, Banana, Laranja]

ricardotecpro.github.io