W3docs

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).
  • mapToObj em um stream primitivo → de volta para Stream<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 stops

Nã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

StatelessStateful
filterdistinct
map / mapToXsorted
flatMap / mapMultilimit
peekskip
boxed / asLongStream / asDoubleStreamtakeWhile / 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.

java— editable, runs on the server

O que tirar da execução:

  • filter e map sã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.
  • flatMap e mapMulti são a forma de uma entrada se tornar várias saídas. O formato Stream.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.
  • distinct e sorted são statefuldistinct precisou lembrar cada Person emitida anteriormente para eliminar a duplicata "Alice", e sorted precisou armazenar toda a entrada em buffer. É por isso que ambos são colocados deliberadamente, geralmente uma vez, e geralmente no final.
  • peek disparou uma vez por elemento puxado no iterate infinito — havia exatamente tantas linhas "considered N" quantos elementos findFirst precisava examinar. Sem short-circuiting esse pipeline nunca terminaria.
  • Os dois blocos de contagem de lookup no 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.

Prática

Prática
Em qual pipeline `sorted` precisa armazenar em buffer *todos* os elementos de entrada antes de poder emitir sequer uma saída?
Em qual pipeline `sorted` precisa armazenar em buffer *todos* os elementos de entrada antes de poder emitir sequer uma saída?
Was this page helpful?