Operações Intermediárias de Stream em Java
Transforme streams Java de forma lazy com filter, map, flatMap, sorted, distinct, peek, limit e skip.
Uma operação intermediária recebe um stream e retorna outro stream. Ela registra o que deve acontecer com cada elemento quando o pipeline eventualmente executar; ela não executa nada por conta própria. Você as encadeia; a cadeia permanece inativa até que um terminal puxe o primeiro elemento. Essa preguiça é o que faz um pipeline de 30 linhas custar menos do que suas partes, o que torna fontes infinitas tratáveis, e o que faz a escolha da operação ser mais sobre clareza do que sobre evitar trabalho — intermediárias adjacentes se fundem em uma única passagem.
Este capítulo é um tour por todas as intermediárias que você vai escrever. Cada entrada tem o mesmo formato: o que ela faz, qual é o tipo do seu callback, se ela é stateless ou stateful, e uma ou duas armadilhas que determinam se o pipeline está correto.
filter — manter o que corresponde
Descarta elementos que falham em um Predicate<T>:
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0)
.toList();Stateless, lazy, preserva a ordem. O predicado deve ser livre de efeitos colaterais — se ele mutar qualquer coisa visível, pipelines paralelos vão surpreender você e até os sequenciais ficam difíceis de ler.
filter não muda o tipo do elemento. Para manter um subconjunto e mudar o tipo, use filter e depois map, ou mapMulti (Java 16+) para o caso raro em que uma entrada se torna zero ou uma saída de tipo diferente.
map — transformar cada elemento
Aplica uma Function<T, R> a cada elemento, produzindo um stream de R:
List<Integer> lengths = words.stream()
.map(String::length)
.toList();Stateless, lazy, preserva a ordem, uma entrada uma saída. Use as especializações primitivas quando o resultado for numérico:
mapToInt,mapToLong,mapToDouble→ stream primitivo (sem boxing,sum()disponível).mapToObjem um stream primitivo → de volta paraStream<R>.
int totalLength = words.stream().mapToInt(String::length).sum();flatMap — substituir cada elemento por um stream de outros
Uma Function<T, Stream<R>> que "desempacota" cada elemento em múltiplas saídas (ou nenhuma, ou uma):
List<List<String>> grouped = List.of(List.of("a", "b"), List.of("c"));
List<String> flat = grouped.stream()
.flatMap(List::stream)
.toList(); // [a, b, c]O modelo mental: "cada elemento se torna um sub-stream, e flatMap os concatena." É assim que você vai de um stream de contêineres (Stream<List<T>>) para um stream de conteúdos (Stream<T>), como você expande cada texto em suas palavras, e como você transforma um stream de Optional<T> em um stream de valores presentes (via Optional::stream).
Existem especializações primitivas também — flatMapToInt, flatMapToLong, flatMapToDouble — para fan-out em um stream primitivo.
Uma confusão comum: map(s -> s.split(" ")) produz Stream<String[]> — um stream de arrays, não um stream plano de palavras. Para achatar, use flatMap(s -> Arrays.stream(s.split(" "))).
mapMulti — emitir zero, um ou muitos elementos por entrada
mapMulti (Java 16+) é um flatMap mais eficiente para casos em que cada entrada produz um número pequeno e variável de saídas e construir um Stream por elemento é excessivo:
people.stream()
.<String>mapMulti((p, downstream) -> {
if (p.age() >= 18) downstream.accept(p.name());
if (p.email() != null) downstream.accept(p.email());
})
.forEach(System.out::println);Use flatMap quando você naturalmente tem um stream/lista para emitir; use mapMulti quando você de outra forma construiria um tiny stream de um ou dois elementos por entrada apenas para satisfazer a assinatura do flatMap.
distinct — remover duplicatas
Remove elementos iguais usando equals / hashCode:
List<String> unique = words.stream().distinct().toList();Stateful — para saber se um elemento é duplicata, distinct precisa lembrar os que já emitiu. Em um stream ordenado, mantém a primeira ocorrência. Em um stream não ordenado, a JVM pode ser mais inteligente com trabalho paralelo. Em um stream infinito, você quase nunca quer distinct sem um limit upstream.
sorted — ordenar os elementos
Duas formas — ordem natural e um Comparator<T>:
List<String> az = words.stream().sorted().toList();
List<String> byLen = words.stream().sorted(Comparator.comparingInt(String::length)).toList();Stateful e bloqueante no terminal: sorted precisa armazenar em buffer todos os elementos antes de poder emitir um. Isso o torna a intermediária mais cara e uma a ser usada deliberadamente. Colocá-lo antes de um limit(n) não economiza trabalho — a JVM ainda precisa ver todas as entradas para saber quais n manter. (Para um pipeline "top N", prefira um PriorityQueue limitado ou Collectors.toList() e depois subList após um sorted, dependendo de N versus o total.)
Além disso: não chame sorted em um stream de uma fonte infinita — ele nunca retorna.
peek — observar sem alterar
Um Consumer<T> que dispara para cada elemento puxado. Retorna o stream inalterado:
words.stream()
.peek(s -> System.out.println("seen: " + s))
.filter(s -> s.length() > 3)
.toList();Apenas para depuração. peek executa de forma lazy e exatamente uma vez por elemento puxado, então é uma janela útil para a preguiça e o short-circuiting:
Stream.iterate(1, n -> n + 1)
.peek(n -> System.out.println("considered " + n))
.filter(n -> n > 100)
.findFirst(); // pulls 1..101 -- peek fires 101 times, then stopsNão coloque lógica real em um peek. A JVM pode fundir, reordenar ou pular chamadas de peek sob certas condições em streams não modificados, e em streams paralelos a ordem é indefinida.
limit(n) — manter no máximo n elementos
Para o pipeline após n elementos passarem:
List<Integer> firstFive = Stream.iterate(1, i -> i + 1).limit(5).toList();Stateful (conta) e short-circuiting (o downstream para assim que n é atingido). Em um stream ordenado mantém os primeiros n. Em um stream paralelo não ordenado mantém algum n — a ordem não é garantida, e um limit paralelo em um stream ordenado paga pelo ordenamento. Se você não se importa com qual n você obtém, stream.unordered().limit(n) é mais rápido em paralelo.
O padrão padrão para domar qualquer fonte infinita: todo Stream.iterate / Stream.generate termina em um limit, um iterate de 3 argumentos limitado, ou um terminal short-circuiting como findFirst.
skip(n) — descartar os primeiros n
O complemento de limit. Descarta os primeiros n elementos e emite o restante:
List<Integer> rest = nums.stream().skip(2).toList(); // drops nums[0], nums[1]Stateful (conta regressiva). Em um stream ordenado o significado é exato; em um stream paralelo ordenado há um custo de ordenamento. Junto com limit, fornece acesso "paginado":
list.stream().skip(page * pageSize).limit(pageSize).toList();Isso funciona, mas para skip grande sobre uma List ainda é O(skip + limit). Um list.subList(...) direto é mais barato se você tiver a List em mãos.
takeWhile / dropWhile — janelamento baseado em prefixo
Duas intermediárias short-circuiting (Java 9+) que atuam em um prefixo do stream:
// take elements while predicate holds, stop at the first miss
List<Integer> small = Stream.of(1, 2, 3, 10, 4, 5)
.takeWhile(n -> n < 5)
.toList(); // [1, 2, 3]
// drop elements while predicate holds, then emit the rest
List<Integer> rest = Stream.of(1, 2, 3, 10, 4, 5)
.dropWhile(n -> n < 5)
.toList(); // [10, 4, 5]Estas não são filter. filter testa cada elemento. takeWhile para na primeira falha (incluindo as que passariam em filter depois). Em um stream ordenado são a forma de expressar "tudo até o limiar" de forma barata.
boxed / asLongStream / asDoubleStream — mover entre mundos primitivos
Streams primitivos têm algumas intermediárias próprias para cruzar de volta ao mundo de objetos:
IntStream.range(0, 5).boxed().toList(); // Stream<Integer> [0, 1, 2, 3, 4]
IntStream.range(0, 3).asLongStream().sum(); // 0L + 1L + 2L
IntStream.range(0, 3).asDoubleStream().average();boxed é a ponte de primitivo para Stream<Integer/Long/Double>. O inverso é mapToInt/mapToLong/mapToDouble.
Stateless vs. stateful — por que isso importa
| Stateless | Stateful |
|---|---|
filter | distinct |
map / mapToX | sorted |
flatMap / mapMulti | limit |
peek | skip |
boxed / asLongStream / asDoubleStream | takeWhile / dropWhile |
Intermediárias stateful precisam lembrar algo entre os elementos. sorted precisa armazenar tudo em buffer. distinct precisa lembrar cada elemento já emitido. limit e skip precisam de um contador. Isso as torna mais caras (especialmente em paralelo) e vale a pena usá-las deliberadamente.
A ordem importa — fundir, filtrar cedo, transformar tarde
Como intermediárias adjacentes se fundem em uma única passagem elemento a elemento, a ordem em que você as escreve determina quanto trabalho o pipeline faz:
// Good: filter first, then the expensive map runs only on survivors.
people.stream()
.filter(p -> p.age() >= 18)
.map(this::expensiveLookup)
.toList();
// Bad: every element pays for the map, then most are thrown away.
people.stream()
.map(this::expensiveLookup)
.filter(r -> r.score() > 0.5)
.toList();A regra geral: filtre cedo, transforme tarde, ordene uma vez, elimine duplicatas uma vez. A JVM não reordena suas intermediárias — você faz isso.
Um exemplo prático: todo o vocabulário em um pipeline
O programa abaixo constrói um stream a partir de uma lista pequena, percorre todas as operações que cobrimos, imprime o resultado de cada uma e prova a preguiça/short-circuiting com peek mais um iterate infinito.
O que tirar da execução:
filteremapsão os carros-chefe; as outras intermediárias de uma entrada-uma saída (mapToInt,mapToObj,boxed) são as conversões de moeda baratas entre streams de objetos e primitivos.flatMapemapMultisão a forma de uma entrada se tornar várias saídas. O formatoStream.of("a b") -> Arrays.stream(split(...))é o padrão canônico de "tokenização";mapMultié a escolha mais barata quando você de outra forma construiria um stream minúsculo por elemento.distinctesortedsão stateful —distinctprecisou lembrar cadaPersonemitida anteriormente para eliminar a duplicata "Alice", esortedprecisou armazenar toda a entrada em buffer. É por isso que ambos são colocados deliberadamente, geralmente uma vez, e geralmente no final.peekdisparou uma vez por elemento puxado noiterateinfinito — havia exatamente tantas linhas "considered N" quantos elementosfindFirstprecisava examinar. Sem short-circuiting esse pipeline nunca terminaria.- Os dois blocos de contagem de
lookupno final tornaram a regra de ordem concreta. Filtrar primeiro executou a transformação cara em muito menos elementos do que mapear primeiro. Essa troca é sua para definir.
O que vem a seguir
As intermediárias registram a forma do trabalho; nada executa até que um terminal puxe. O próximo capítulo, Operações Terminais de Stream em Java, é o vocabulário completo de terminais — forEach, count, min/max, findFirst/findAny, anyMatch/allMatch/noneMatch, reduce, toArray, toList, e o portal para o capítulo seguinte — collect.