Operações Terminais de Stream em Java
Dispare a avaliação de streams em Java com operações terminais — collect, forEach, reduce, count, min, max, anyMatch.
Uma operação terminal é o que faz um pipeline de stream realmente executar. Operações intermediárias (filter, map, sorted, …) apenas registram o trabalho e permanecem preguiçosas; uma terminal puxa os elementos, avalia todo o pipeline e produz um resultado (ou um efeito colateral). Cada pipeline termina em exatamente uma terminal — chame-a, e o stream é consumido; chame outra terminal no mesmo stream e você obterá IllegalStateException.
Este capítulo cobre todas as terminais que você escreverá, quando cada uma causa curto-circuito e os casos extremos de stream vazio que costumam pegar as pessoas de surpresa.
As terminais têm três formatos. Agregadores retornam um único valor (count, sum, min, max, reduce). Buscadores procuram um elemento e param (findFirst, findAny, anyMatch, allMatch, noneMatch). Construtores materializam o stream em um contêiner (toList, toArray, collect, forEach para efeitos colaterais). Este capítulo percorre todas as terminais que você escreverá, exceto collect, que é grande o suficiente para precisar de seu próprio capítulo a seguir.
forEach / forEachOrdered — efeitos colaterais
A terminal mais simples. Executa um Consumer<T> para cada elemento, não retorna nada:
names.stream().forEach(System.out::println);A ordem não é garantida — em um stream sequencial geralmente é; em um stream paralelo, não. Se você precisar da ordem da fonte mesmo em paralelo, use forEachOrdered:
names.parallelStream().forEachOrdered(System.out::println);forEach é para efeitos colaterais que você genuinamente deseja — logging, mutação de um sink, chamada de uma API não-stream. Não é a maneira correta de construir uma coleção (isso é toList / collect) ou acumular um valor (isso é reduce). Um forEach que muta uma lista externa é um code smell mesmo quando funciona, porque abandona tudo que tornava o pipeline declarativo.
count — quantos elementos
Retorna um long:
long adults = people.stream().filter(p -> p.age() >= 18).count();count causa curto-circuito em fontes dimensionadas onde a JVM pode calcular a resposta pelo tamanho da fonte (portanto IntStream.range(0, 1_000_000).count() retorna 1000000 sem iterar). Em um stream com um filter ou flatMap ativo, ele precisa percorrer cada elemento.
Uma armadilha comum: stream.count() em uma cadeia .peek(...) pode não executar o peek se a JVM puder provar o count pela fonte, porque não há diferença de comportamento observável. Não use peek para "ver quantos foram filtrados" — use mapToInt(x -> 1).sum() ou reestruture.
min / max — elementos extremos
Ambos recebem um Comparator<T> e retornam Optional<T> (porque o stream pode estar vazio):
Optional<Person> oldest = people.stream().max(Comparator.comparingInt(Person::age));
Optional<String> shortest = words.stream().min(Comparator.comparingInt(String::length));As especializações primitivas são mais simples — IntStream.max() retorna OptionalInt, sem necessidade de comparator:
OptionalInt highest = nums.stream().mapToInt(Integer::intValue).max();
int hi = highest.orElse(Integer.MIN_VALUE);min/max causam curto-circuito apenas em fontes limitadas. Em um stream infinito, max nunca termina.
findFirst / findAny — obter um elemento
Ambos retornam Optional<T>, ambos causam curto-circuito. A diferença é o que prometem sobre qual elemento você obtém:
Optional<Person> first = people.stream().filter(p -> p.age() >= 30).findFirst();
Optional<Person> any = people.stream().filter(p -> p.age() >= 30).findAny();findFirstretorna o primeiro elemento na ordem de encontro. Em um stream sequencial é literalmente o primeiro. Em um stream paralelo custa mais quefindAnyporque a JVM precisa coordenar.findAnyretorna algum elemento correspondente — o primeiro que qualquer worker encontrar. Em paralelo é mais barato. Em sequencial, ambos retornam a mesma coisa.
Use findAny quando qual correspondência você obtém genuinamente não importa (é uma verificação de existência simples que precisa do valor, não apenas um boolean). Use findFirst quando você quer dizer "o primeiro."
anyMatch / allMatch / noneMatch — quantificadores de existência
Recebem um Predicate<T> e retornam boolean. Os três causam curto-circuito:
boolean hasAdult = people.stream().anyMatch(p -> p.age() >= 18);
boolean allAdult = people.stream().allMatch(p -> p.age() >= 18);
boolean noChildren = people.stream().noneMatch(p -> p.age() < 13);anyMatchpara assim que um elemento passa.allMatchpara assim que um elemento falha.noneMatché!anyMatch(p)— para no primeiro passe e retornafalse.
Semântica de stream vazio (a regra que pega todo mundo uma vez): anyMatch em vazio é false. allMatch e noneMatch em vazio são ambos true — vacuamente, porque não há contra-exemplos. Isso pode ser exatamente o que você quer ou exatamente o que você não quer, dependendo da pergunta. Se "vazio" é uma possibilidade que vale a pena tratar, verifique isEmpty (ou count() == 0) primeiro.
reduce — dobrar para um único valor
O agregador mais geral. Três sobrecargas, cada uma para um formato ligeiramente diferente:
reduce(identity, accumulator) com dois argumentos — dobrar com um valor inicial, retorna T (sem Optional, porque identity é a resposta para um stream vazio):
int sum = nums.stream().reduce(0, Integer::sum);
String all = words.stream().reduce(\"\", String::concat);reduce(accumulator) com um argumento — sem identity; retorna Optional<T> para o caso de stream vazio:
Optional<Integer> sum = nums.stream().reduce(Integer::sum);
Optional<String> longest = words.stream()
.reduce((a, b) -> a.length() >= b.length() ? a : b);reduce(identity, accumulator, combiner) com três argumentos — usado quando o acumulador produz um tipo diferente dos elementos (e obrigatório em paralelo). O combiner mescla dois resultados parciais:
int totalLength = words.stream()
.reduce(0,
(acc, w) -> acc + w.length(), // BiFunction<Integer, String, Integer>
Integer::sum); // BinaryOperator<Integer>Três regras para reduce que evitam que o pipeline vá sutilmente errado:
- O acumulador deve ser associativo:
f(f(a, b), c) == f(a, f(b, c)). Somas e concatenação de string satisfazem isso; subtração não. - Identity deve ser uma verdadeira identidade:
f(id, x) == xpara todox.0para+,1para*,\"\"paraconcat. - O acumulador e o combiner devem ser sem estado e livres de efeitos colaterais.
Viole qualquer uma dessas regras e um pipeline sequencial ainda dá a resposta certa na maioria das vezes — um paralelo irá surpreendê-lo. (Este é o mesmo contrato em que Collectors.reducing e reduce paralelo se baseiam.)
sum / average — agregadores primitivos
Apenas em streams primitivos. sum retorna o primitivo; average retorna um OptionalDouble:
int total = IntStream.rangeClosed(1, 100).sum();
OptionalDouble avg = nums.stream().mapToInt(Integer::intValue).average();
double mean = avg.orElse(0.0);Para resumos numéricos mais ricos — count, sum, min, max, average em uma única passagem — veja IntSummaryStatistics:
IntSummaryStatistics stats = nums.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println(stats); // {count=N, sum=..., min=..., average=..., max=...}Isso é uma passagem, uma alocação e muito mais barato do que calcular cada um separadamente.
toArray e toList — materializar
Duas terminais de atalho "me dê tudo":
Object[] anyArr = stream.toArray(); // Object[]
String[] strArr = stream.toArray(String[]::new); // typed via constructor ref
List<String> immutable = stream.toList(); // Java 16+, unmodifiablestream.toList() (Java 16+) é a forma moderna de materializar um stream em uma List e é a escolha certa em 95% das vezes. É imutável e pode conter nulls; se você precisar de uma lista mutável, uma implementação específica ou um Set/Map, recorra a collect(Collectors.toCollection(ArrayList::new)) ou seus similares no próximo capítulo.
toArray(T[]::new) é a única forma de obter um array tipado de um stream de objetos — a forma IntFunction<T[]> fornece ao runtime o tipo do componente do array.
iterator e spliterator — saídas de emergência
Um stream pode ser convertido em um Iterator<T> ou Spliterator<T> para entrega a código que espera um:
for (Iterator<String> it = stream.iterator(); it.hasNext(); ) {
use(it.next());
}Ambos são terminais — consomem o stream. Existem para interoperabilidade, não para "quero um loop for"; se você quer um loop, use um sem criar um stream primeiro.
Curto-circuito vs. consumo — a tabela de segurança
| Terminal | Causa curto-circuito em fonte infinita? |
|---|---|
findFirst / findAny | sim |
anyMatch / allMatch / noneMatch | sim |
limit(n) (intermediário) depois de qualquer coisa | sim |
forEach / forEachOrdered | não — consome tudo |
count | não — consome tudo |
min / max | não — consome tudo |
reduce | não — consome tudo |
sum / average / summaryStatistics | não — consome tudo |
toList / toArray / collect | não — consome tudo |
O padrão é claro: qualquer terminal que precisa considerar cada elemento para produzir sua resposta não causa curto-circuito, e associá-la a uma fonte infinita sem um limit upstream trava a JVM. Buscadores e quantificadores são as únicas terminais "seguras em infinito".
Um exemplo trabalhado: todos os formatos de terminal em um pipeline
O programa abaixo constrói um stream pequeno, chama cada terminal que cobrimos e mostra as respostas de stream vazio para os três matchers e para min / findFirst / reduce.
O que observar na execução:
- As terminais de "busca" —
findFirst,findAny,anyMatch,allMatch,noneMatch— e as terminais de "consumo total" —count,min/max,reduce,sum,toList— dividem o capítulo claramente. As terminais de busca causam curto-circuito; as de consumo total não. Associe o segundo grupo a uma fonte infinita somente após umlimit. allMatchem um stream vazio retornoutrue. O mesmo feznoneMatch. Isso é verdade vacua — é a resposta padrão, e é a razão mais comum pela qual o código de produção "passa incorretamente" em um caso extremo de entrada vazia. Se vazio é significativo, verifique primeiro.- As três sobrecargas de
reducecobrem três padrões. Com dois argumentos e uma verdadeira identidade retornaT. Com um argumento retornaOptional<T>porque não há identidade. Com três argumentos permite que o tipo do acumulador difira do tipo do elemento — e é a forma que é realmente segura em paralelo, porque o combiner diz à JVM como mesclar resultados parciais. summaryStatistics()fez em uma passagem o que chamarmin,max,sum,averageecountseparadamente teria feito em cinco. Em qualquer stream numérico não trivial, prefira-o.toList()retornou uma lista imutável. Esse é o padrão do Java 16+ e quase sempre o que você quer; o próximo capítulo mostra a formaCollectors.toCollection(...)quando você precisa de uma mutável, uma implementação específica ou umSet/Map.
O que vem a seguir
collect é a única terminal que adiamos — e a porta de entrada para metade da API. O próximo capítulo, Java Stream Collectors, percorre a caixa de ferramentas Collectors: toList/toSet/toMap, groupingBy, partitioningBy, joining, counting, summingInt, averagingDouble, mapping, reducing e o padrão downstream que os compõe.