W3docs

Introdução aos Java Streams

Uma introdução à Java Stream API para processar sequências de elementos com operações de estilo funcional.

Um stream é um pipeline que conduz os elementos de uma fonte por uma sequência de operações e produz um resultado. Ele não é uma estrutura de dados — não armazena nada. É uma receita declarativa para processar dados, avaliada de forma lazy e executada uma única vez. Os Streams chegaram no Java 8 junto com lambdas, e os dois foram projetados para se encaixar: cada operação de stream recebe uma função, e a linguagem ofereceu uma forma limpa de escrevê-la.

O formato que você escreverá centenas de vezes:

double avgAdultAge = people.stream()
    .filter(p -> p.age() >= 18)
    .mapToInt(Person::age)
    .average()
    .orElse(0.0);

Três coisas a observar. O pipeline é lido de cima para baixo como etapas que descrevem o que você quer, não como iterar. Cada etapa recebe uma função — um Predicate, um ToIntFunction — exatamente o vocabulário estabelecido nos capítulos anteriores. E o resultado sai de uma única operação terminal; não há loop, não há acumulador, não há continue antecipado.

O formato do pipeline: fonte → intermediária → terminal

Todo pipeline de stream tem três partes:

  1. Uma fonte. De onde os elementos vêm. Geralmente uma coleção (coll.stream()), ocasionalmente um literal (Stream.of(\"a\", \"b\")), um array (Arrays.stream(arr)), um intervalo de IntStream (IntStream.range(0, 100)), uma fonte de I/O (Files.lines(path)), ou um gerador (Stream.iterate, Stream.generate). O próximo capítulo é dedicado a todos eles.
  2. Zero ou mais operações intermediárias. Cada uma retorna outro stream, portanto podem ser encadeadas. As mais comuns: filter, map, flatMap, distinct, sorted, limit, skip, peek. Elas são lazy — chamar filter não testa nada ainda; apenas registra o predicado.
  3. Exatamente uma operação terminal. Dispara o pipeline. Exemplos: forEach, collect, toList, count, sum, min, max, reduce, findFirst, anyMatch. O terminal produz um valor (ou um efeito colateral para forEach) e consome o stream — você não pode reutilizá-lo.
list.stream()              // SOURCE
    .filter(...)           // intermediate
    .map(...)              // intermediate
    .sorted()              // intermediate
    .toList();             // TERMINAL — runs the pipeline

Sem o terminal, nada acontece. Um stream que você constrói e nunca termina é peso morto — nenhum trabalho é feito, nenhum efeito colateral ocorre, os lambdas não executam.

Lazy por design

As operações intermediárias são lazy porque a JVM não sabe quais elementos você realmente precisa até que o terminal solicite. Isso possibilita duas otimizações importantes:

Fusão. Intermediárias adjacentes executam juntas em uma única passagem, não uma passagem por operação. stream.filter(p).map(f) não constrói uma lista filtrada intermediária e depois a mapeia; ele testa um elemento e, se ele passar, o mapeia, tudo em uma única etapa.

Short-circuiting. Um terminal como findFirst, anyMatch ou limit(n) interrompe o pipeline assim que tem sua resposta. Combinado com a avaliação lazy, isso significa que você pode executar um pipeline "encontre o primeiro quadrado par maior que 100" sobre um stream infinito e obter uma resposta em microssegundos:

int answer = Stream.iterate(1, n -> n + 1)         // 1, 2, 3, 4, ...
    .map(n -> n * n)                                // 1, 4, 9, 16, ...
    .filter(n -> n % 2 == 0 && n > 100)             // first match wins
    .findFirst()
    .orElseThrow();
// answer = 144

Stream.iterate(1, n -> n + 1) é infinito, mas findFirst solicitou elementos apenas até que um correspondesse. O pipeline testou 12 quadrados (1, 4, 9, ..., 144) e parou.

De uso único, como um Iterator

Um Stream pode ser percorrido uma vez. O terminal o consome e, depois disso, o objeto stream é fechado; chamar outro terminal nele lança IllegalStateException:

Stream<String> s = list.stream();
long c1 = s.count();             // ok
long c2 = s.count();             // throws IllegalStateException — stream has already been operated upon

Se você precisar processar os mesmos dados duas vezes, construa o stream duas vezes:

long c1 = list.stream().count();
long c2 = list.stream().count();

Isso corresponde ao funcionamento do Iterator. O objeto stream é o cursor em movimento, não os dados. Os dados são a fonte — fazer streaming novamente é gratuito.

Streams vs coleções — funções diferentes

AspectoCollectionStream
Armazena dados?SimNão
Reutilizável?SimNão (um terminal)
Eager ou lazy?EagerLazy até o terminal
Modifica a fonte?Sim (ex.: list.add)Não — pipelines são somente leitura
Itera explicitamente?Frequentemente (for, iterator())Não — o pipeline conduz a iteração
Modelo de custoBookkeeping por elementoUma passagem pela fonte

Uma coleção é um contêiner; um stream é uma computação sobre um contêiner (ou outra fonte). Eles se complementam: você busca de uma coleção, executa um pipeline de stream e coleta de volta em uma coleção (geralmente diferente).

Três pequenos exemplos que você escreverá o tempo todo

Contando elementos que correspondem a um predicado:

long adults = people.stream().filter(p -> p.age() >= 18).count();

Construindo uma lista de valores transformados:

List<String> names = people.stream().map(Person::name).toList();

Reduzindo a um único valor:

int totalAge = people.stream().mapToInt(Person::age).sum();

Esses três padrões — count, map-to-list, reduce-to-scalar — cobrem a maioria dos usos da API. O restante da parte é um tour pelas operações que preenchem o como para cada um.

Três coisas que os streams não são

  • Não são uma substituição para loops for em geral. Um loop que constrói algo com controle de fluxo não trivial, que precisa de break com efeitos colaterais, ou que muta várias variáveis, ainda é mais claro como um loop. Os Streams brilham quando o trabalho é um pipeline de operações puras.
  • Não são um ganho de desempenho em dados pequenos. Um pipeline de stream aloca alguns objetos pequenos; um loop de 10 elementos vai superá-lo. Os ganhos vêm da clareza em qualquer dado e do paralelismo em grandes volumes de dados.
  • Não são substitutos para Iterator/Iterable quando outro código os espera. Um stream produz valores; se você precisa intercalar o consumo (um for aprimorado, um List retornado de um método), use toList() primeiro.

Sequencial por padrão, paralelo sob demanda

Todo stream que você escreverá neste capítulo é sequencial — os elementos fluem pelo pipeline um de cada vez, em ordem. Há também coll.parallelStream() (e stream.parallel()) que agenda o pipeline na ForkJoinPool comum para trabalho multi-core. Streams paralelos são um capítulo posterior — eles fazem várias suposições sobre o pipeline (ele deve ser associativo, sem estado, sem efeitos colaterais) que os pipelines de "introdução" deste capítulo naturalmente satisfazem, portanto a atualização geralmente é uma mudança de um token.

Um exemplo prático: um pipeline completo, laziness e a regra de uso único

O programa abaixo constrói uma pequena lista de registros Person, executa o formato canônico de pipeline (filter → map → sorted → collect), prova a laziness com peek, demonstra o short-circuiting em um Stream.iterate infinito e mostra o IllegalStateException que você obtém ao reutilizar um stream.

java— editable, runs on the server

O que observar na execução:

  • O pipeline canônico de quatro etapas — streamfiltermaptoList — produziu uma lista ordenada de nomes de adultos sem nenhum loop explícito, sem coleção temporária e sem gerenciamento de nullabilidade.
  • peek imprimiu uma vez por elemento puxado. findFirst puxou elementos até que um satisfizesse n*n > 50 (o que acontece em n = 8, quadrado 64) e então parou. É a laziness e o short-circuiting trabalhando juntos: as operações upstream fizeram exatamente o trabalho necessário e nada mais.
  • O pipeline "primeiro quadrado par acima de 100" executou sobre uma fonte infinita. Sem short-circuiting isso seria um loop infinito; com ele o pipeline testou 12 valores e produziu 144.
  • O segundo s.count() lançou IllegalStateException. Streams são de uso único; se você precisar de uma segunda passagem, construa um stream novo da fonte.
  • O pipeline "sem terminal" no final não imprimiu nada de dentro do seu peek. Sem um terminal, os intermediários não executam — o stream é apenas uma receita que ninguém pediu para executar.

O que vem a seguir

Você conhece o formato do pipeline, a divisão fonte/intermediária/terminal, o contrato de laziness e a regra de uso único. O próximo capítulo, Criando Java Streams, é o catálogo de fontesCollection.stream(), Stream.of, Arrays.stream, IntStream.range, Stream.iterate, Stream.generate, Files.lines, String.chars(), Stream.empty e a API Stream.Builder. Com o capítulo de fontes concluído, você terá tudo o que precisa para começar, e o restante da parte preencherá as operações intermediárias e terminais.

Prática

Prática
Você escreve `list.stream().filter(p).map(f);` e não chama nenhuma operação terminal. O que acontece quando essa linha é executada?
Você escreve `list.stream().filter(p).map(f);` e não chama nenhuma operação terminal. O que acontece quando essa linha é executada?
Was this page helpful?