VapVup👤
← Voltar
🌊

Streams e Functional Programming em Java

A Stream API revolucionou o Java 8 ao trazer programação funcional para o ecossistema. Lambdas, streams e Optional eliminam boilerplate, tornam o código mais declarativo e permitem otimizações que o compilador faz por você. Mas atenção: usar streams sem entender o que acontece por baixo é como dirigir automático sem saber o que é embreagem — funciona até o dia que não funciona.

📝Analogia

Lambda — O Bilhete Anônimo

Imagine que em vez de contratar um funcionário registrado (classe anônima) para entregar uma mensagem, você escreve num bilhete e entrega direto. Lambda é isso: um pedaço de comportamento passado como argumento, sem a cerimônia de criar uma classe. A JVM usa invokedynamic por baixo dos panos, o que é mais eficiente que classes anônimas em muitos cenários.

💡Conceito

Functional Interfaces — O Contrato de Um Método

Uma functional interface tem exatamente UM método abstrato (SAM). As principais são: Function<T,R> (transforma T em R), Predicate<T> (testa e retorna boolean), Consumer<T> (consome sem retorno), Supplier<T> (fornece sem input) e BiFunction<T,U,R> (dois inputs). UnaryOperator<T> estende Function<T,T> para quando input e output são do mesmo tipo. Composição: Function tem andThen() e compose(); Predicate tem and(), or() e negate().

🔧Exemplo

Method References — Atalhos Elegantes

Method references são açúcar sintático para lambdas simples que apenas delegam para um método existente. Existem quatro tipos: referência a método estático (Integer::parseInt), referência a método de instância de um objeto particular (myList::add), referência a método de instância de um tipo arbitrário (String::toUpperCase), e referência a construtor (ArrayList::new).

// Tipo 1: Static method reference
Function<String, Integer> parser = Integer::parseInt;

// Tipo 2: Instance method of particular object
List<String> list = new ArrayList<>();
Consumer<String> adder = list::add;

// Tipo 3: Instance method of arbitrary object
Function<String, String> upper = String::toUpperCase;

// Tipo 4: Constructor reference
Supplier<List<String>> factory = ArrayList::new;
🏭Analogia

Streams — A Esteira Industrial

Um stream é como uma esteira de fábrica: os itens (dados) passam por estações de trabalho (operações intermediárias) uma peça por vez, não em lote. A esteira só liga quando alguém no final pede o produto (operação terminal). Se ninguém pede nada, a esteira fica parada — isso é lazy evaluation. E depois que o produto passa, a esteira não rebobina — streams são single-use.

💡Conceito

Criando Streams — As Fontes de Dados

Há várias formas de criar um stream: collection.stream() de qualquer Collection, Arrays.stream(array) de arrays, Stream.of(valores) de valores avulsos, Stream.generate(supplier) para streams infinitos, Stream.iterate(seed, operator) para sequências, e IntStream.range(start, end) para primitivos. Streams de primitivos (IntStream, LongStream, DoubleStream) evitam autoboxing e oferecem operações especializadas como sum() e average().

🔧Exemplo

Pipeline Completo — filter, map, collect

O pipeline típico segue o padrão: source → operações intermediárias → operação terminal. Cada operação intermediária retorna um novo Stream (imutável). O processamento é vertical: cada elemento percorre todo o pipeline antes do próximo começar. Isso permite short-circuiting com operações como findFirst() e limit().

// Encontrar os 3 emails mais longos de usuários ativos
List<String> topEmails = users.stream()          // source
    .filter(User::isActive)                       // intermediária
    .map(User::getEmail)                          // intermediária
    .filter(email -> email.length() > 10)         // intermediária
    .sorted(Comparator.comparingInt(String::length).reversed())
    .limit(3)                                     // intermediária (short-circuit)
    .collect(Collectors.toList());                // terminal

// flatMap: achatar listas aninhadas
List<String> allTags = posts.stream()
    .flatMap(post -> post.getTags().stream())     // List<List<Tag>> → Stream<Tag>
    .map(Tag::getName)
    .distinct()
    .collect(Collectors.toList());
📦Analogia

Collectors — O Empacotador no Fim da Esteira

Se o stream é a esteira, o Collector é o empacotador no final. Ele decide como os itens processados serão embalados: numa caixa (List), num saco sem repetidos (Set), num catálogo indexado (Map), ou colados num texto só (joining). Collectors compostos como groupingBy com downstream são como empacotadores que separam em caixas e depois contam ou resumem o conteúdo de cada uma.

🔧Exemplo

Collectors Avançados — groupingBy e reduce

Collectors.groupingBy() é o equivalente do GROUP BY do SQL. Aceita um classificador e um downstream collector opcional. reduce() acumula elementos em um único valor — o identity deve ser o elemento neutro da operação. Cuidado: com parallel streams, um identity errado gera resultados incorretos porque é usado em cada partição.

// Agrupar por departamento e calcular média salarial
Map<String, Double> avgSalary = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// Particionar aprovados/reprovados
Map<Boolean, List<Student>> result = students.stream()
    .collect(Collectors.partitioningBy(s -> s.getGrade() >= 7.0));

// Reduce: concatenar com separador
String names = List.of("Ana", "Bruno", "Carlos").stream()
    .reduce((a, b) -> a + ", " + b)
    .orElse("");  // reduce sem identity retorna Optional
💡Conceito

Optional — Null Nunca Mais (ou Quase)

Optional<T> é um container que pode ou não conter um valor. Use Optional.of(valor) quando tem certeza que não é null, Optional.ofNullable(valor) quando pode ser null, e Optional.empty() para vazio. Encadeie com map(), flatMap(), filter(). Para extrair: orElse() (eager), orElseGet() (lazy), orElseThrow(). NUNCA use Optional como parâmetro de método ou campo de classe — ele foi projetado para retorno de métodos. E nunca faça optional.get() sem isPresent() — isso derrota o propósito.

🔀Analogia

Parallel Streams — A Esteira com Várias Pistas

Parallel stream é como dividir a esteira em várias pistas: cada trabalhador (thread do ForkJoinPool) processa um pedaço, e no final os resultados são combinados. Parece ótimo, mas: se os trabalhadores precisam se comunicar (operações stateful), ficam mais lentos que um só. Se a esteira é difícil de dividir (LinkedList), perde-se tempo cortando. E todos compartilham o mesmo galpão (ForkJoinPool.commonPool()), então uma pista lenta trava as outras tarefas da JVM.

🔧Exemplo

Armadilhas Comuns com Streams

Streams são poderosos mas têm pegadinhas clássicas: reutilização (streams são single-use), side effects em forEach (evite estado mutável), orElse vs orElseGet (eager vs lazy), e parallel streams com shared state. Conheça essas armadilhas para evitá-las em produção e entrevistas.

// ERRO 1: Reutilizar stream
Stream<String> s = list.stream().filter(x -> x.length() > 3);
s.count();  // OK
s.collect(Collectors.toList());  // IllegalStateException!

// ERRO 2: orElse() avalia SEMPRE o argumento
Optional<User> opt = findUser();
User u = opt.orElse(createExpensiveDefault()); // createExpensiveDefault() RODA mesmo se opt tem valor!
User u2 = opt.orElseGet(() -> createExpensiveDefault()); // lazy — só roda se opt está vazio

// ERRO 3: Parallel stream com estado compartilhado
List<String> results = new ArrayList<>();  // NÃO thread-safe!
list.parallelStream().forEach(results::add);  // BUG! Race condition!
// Correto: .collect(Collectors.toList())