W3docs

Java Stream Collectors

Reduza streams Java em coleções e outros resultados com java.util.stream.Collectors.

collect é o terminal que adiamos. Ele recebe um Collector<T, A, R> — uma receita para acumular elementos do stream em um resultado R por meio de um contêiner intermediário A — e o executa. As receitas estão na classe de fábrica java.util.stream.Collectors, e cobrem a maior parte do que você escreveria manualmente com um laço for, um Map e algumas chamadas compute*. Quando você consegue ler groupingBy(..., counting()), a API deixa de parecer enigmática.

O capítulo percorre a caixa de ferramentas pelo que você quer que o resultado seja: uma lista, um conjunto, um mapa, um único número, uma string ou — por meio do padrão downstream — uma combinação aninhada de qualquer um deles.

Listas, conjuntos e coleções específicas

Os dois básicos:

List<String> list = words.stream().collect(Collectors.toList());
Set<String>  set  = words.stream().collect(Collectors.toSet());

Notas:

  • Collectors.toList() retorna alguma List — geralmente mutável, mas sem garantia. Para a forma não modificável que você precisa na maioria das vezes, use stream.toList() (o terminal, não o collector).
  • Collectors.toSet() é não ordenado — tipicamente HashSet. Se você precisar de uma ordem de iteração estável, solicite explicitamente com toCollection(LinkedHashSet::new).
  • Collectors.toUnmodifiableList() e toUnmodifiableSet() (Java 10+) retornam resultados imutáveis — estes são os equivalentes em forma de collector do stream.toList().

Para uma implementação específica, use toCollection:

ArrayDeque<String> queue = words.stream()
    .collect(Collectors.toCollection(ArrayDeque::new));

TreeSet<String> sorted = words.stream()
    .collect(Collectors.toCollection(TreeSet::new));

O supplier é uma referência de construtor; o collector conecta tudo, esvazia o stream nele e o retorna.

toMap — associa cada elemento a uma chave

toMap(keyMapper, valueMapper) transforma cada elemento em um Map.Entry e os acumula:

Map<String, Integer> nameAge = people.stream()
    .collect(Collectors.toMap(Person::name, Person::age));

Chaves duplicadas lançam IllegalStateException. Essa é a regra que pega todo mundo na primeira vez. Se dois Persons compartilham um nome, o toMap padrão falha. A correção é a sobrecarga com função de mesclagem:

Map<String, Integer> sumAgePerName = people.stream()
    .collect(Collectors.toMap(
        Person::name,
        Person::age,
        Integer::sum));                 // merge: existingAge + newAge

Para um tipo de mapa específico — LinkedHashMap para preservar a ordem de inserção, TreeMap para manter as chaves ordenadas — passe um supplier:

Map<String, Integer> ordered = people.stream()
    .collect(Collectors.toMap(
        Person::name, Person::age,
        (a, b) -> a,                    // keep first on collision
        LinkedHashMap::new));

toUnmodifiableMap é a variante imutável (Java 10+).

groupingBy — divide em grupos por chave

O collector ao qual todos recorrem quando percebem que toMap não é a ferramenta certa:

Map<String, List<Person>> byRole = people.stream()
    .collect(Collectors.groupingBy(Person::role));

Para cada elemento, o classificador produz uma chave e o elemento é adicionado ao grupo dessa chave (downstream padrão: toList()). Compare com toMap:

ProduzEm chave duplicada
toMapMap<K, V> (um V por K)Lança exceção a menos que você forneça um merger
groupingByMap<K, List<V>> (um grupo por K)Adiciona ao grupo

Use toMap quando há no máximo um valor por chave por design (id → registro, código → rótulo). Use groupingBy quando há muitos.

O pleno poder do groupingBy vem do seu parâmetro downstream, que diz o que fazer com os elementos que compartilham uma chave. O padrão é toList; você pode substituí-lo por outro collector — e esse collector pode ser ele mesmo um groupingBy. A seção "downstream" do capítulo abaixo é onde a API realmente se abre.

partitioningBy — agrupa por um predicado

Um groupingBy especializado para predicados binários. Retorna um Map<Boolean, List<T>>:

Map<Boolean, List<Person>> adultsOrNot = people.stream()
    .collect(Collectors.partitioningBy(p -> p.age() >= 18));

List<Person> adults  = adultsOrNot.get(true);
List<Person> minors  = adultsOrNot.get(false);

partitioningBy sempre contém as chaves true e false, mesmo que um grupo esteja vazio. Isso é o que ele oferece a mais em relação a groupingBy(p -> p.age() >= 18) — que omitiria a chave se o grupo estivesse vazio.

Assim como groupingBy, partitioningBy aceita um collector downstream.

counting, summingInt, averagingDouble, minBy, maxBy

Os collectors downstream que produzem um único número por grupo:

Map<String, Long> headcount = people.stream()
    .collect(Collectors.groupingBy(Person::role, Collectors.counting()));

Map<String, Integer> totalAgePerRole = people.stream()
    .collect(Collectors.groupingBy(Person::role,
             Collectors.summingInt(Person::age)));

Map<String, Double> avgAgePerRole = people.stream()
    .collect(Collectors.groupingBy(Person::role,
             Collectors.averagingDouble(Person::age)));

Map<String, Optional<Person>> oldestPerRole = people.stream()
    .collect(Collectors.groupingBy(Person::role,
             Collectors.maxBy(Comparator.comparingInt(Person::age))));
  • counting()Long, o tamanho do grupo.
  • summingInt/Long/Double(toX) — soma do primitivo projetado.
  • averagingInt/Long/Double(toX) — média em Double.
  • minBy(cmp) / maxBy(cmp) — extremo em Optional<T>.
  • summarizingInt/Long/Double(toX)IntSummaryStatistics / etc., o pacote completo de contagem/soma/mínimo/máximo/média.

joining — concatena strings

Para streams de CharSequence:

String csv = words.stream().collect(Collectors.joining(","));
String pretty = words.stream().collect(Collectors.joining(", ", "[", "]"));

Três sobrecargas: sem argumento (apenas concatena), um argumento delimitador, três argumentos delimitador + prefixo + sufixo. Mais rápido que reduce("", String::concat) porque usa um StringBuilder internamente e não aloca quadraticamente. A ferramenta certa sempre que o resultado do pipeline é uma única string.

mapping — transforma e então coleta

Envolve outro collector para que os elementos sejam transformados primeiro. O uso mais comum é dentro de groupingBy quando você quer agrupar por algo e coletar uma projeção dos elementos em vez dos próprios elementos:

Map<String, List<String>> namesByRole = people.stream()
    .collect(Collectors.groupingBy(
        Person::role,
        Collectors.mapping(Person::name, Collectors.toList())));

Sem mapping, o toList() downstream coletaria Persons inteiros; com mapping(Person::name, ...), coleta apenas os nomes. Use-o sempre que, de outra forma, escreveria groupingBy(...).entrySet().stream().map(...).collect(...) em duas passagens seguidas.

filtering (Java 9+) é o invólucro correspondente para "descartar alguns antes de coletar":

Map<String, List<Person>> adultsByRole = people.stream()
    .collect(Collectors.groupingBy(
        Person::role,
        Collectors.filtering(p -> p.age() >= 18, Collectors.toList())));

A diferença em relação a stream.filter(...) antes do collector: filtering mantém a chave no mapa de resultado mesmo quando nenhum elemento passa — seu grupo fica simplesmente vazio.

reducing — redução geral completa como collector

A forma collector de reduce, usada como downstream quando os padrões não se encaixam:

Map<String, Optional<Person>> oldestPerRole = people.stream()
    .collect(Collectors.groupingBy(
        Person::role,
        Collectors.reducing(BinaryOperator.maxBy(Comparator.comparingInt(Person::age)))));

Há três sobrecargas (um argumento, dois argumentos com identidade, três argumentos com identidade + mapeador + acumulador) correspondendo às três formas de reduce do capítulo anterior. A forma com dois argumentos é a mais comum como downstream, pois retorna um T simples em vez de Optional<T>.

Raramente você escreve reducing no topo de um pipeline — reduce é o terminal para isso. Você o escreve como downstream de groupingBy/partitioningBy quando quer redução por grupo.

collectingAndThen — pós-processa o resultado

Envolve um collector com uma função finalizadora. O uso padrão é tornar uma List/Map coletada não modificável, ou extrair um valor final de um resultado summarizing*:

List<String> immutableNames = people.stream()
    .map(Person::name)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList));

Map<String, Long> immutableCounts = people.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Person::role, Collectors.counting()),
        Collections::unmodifiableMap));

Também é assim que você transforma groupingBy(..., minBy(...)) em um valor simples em vez de Optional<T> — o finalizador desencapsula o Optional com um padrão conhecido.

teeing — executa dois collectors em uma única passagem

(Java 12+) Alimenta cada elemento em dois collectors ao mesmo tempo e combina seus resultados:

record Range(int min, int max) {}
Range range = nums.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compare),
        Collectors.maxBy(Integer::compare),
        (lo, hi) -> new Range(lo.orElseThrow(), hi.orElseThrow())));

Os dois collectors filhos veem cada elemento; o merger recebe seus dois resultados. Útil quando, de outra forma, você faria stream duas vezes — por exemplo, calcular a média e identificar os valores discrepantes.

Escolhendo o collector certo

Você quer que o resultado sejaUse
List<T> (imutável, caso comum)stream.toList() (terminal)
List<T> (mutável)Collectors.toList() ou toCollection(ArrayList::new)
Set<T>Collectors.toSet() ou toCollection(LinkedHashSet::new)
Coleção específicaCollectors.toCollection(supplier)
Map<K, V> um-para-umCollectors.toMap(k, v) (+ merger se necessário)
Map<K, List<T>> gruposCollectors.groupingBy(k)
Map<Boolean, List<T>>Collectors.partitioningBy(pred)
String únicaCollectors.joining(delim, pre, suf)
Contagem/soma/média por grupogroupingBy(k, counting() / summingInt(...) / ...)
Projeção por grupogroupingBy(k, mapping(proj, toList()))
Extremo por grupogroupingBy(k, minBy(cmp) / maxBy(cmp))
Redução personalizada por grupogroupingBy(k, reducing(...))
Dois resultados em uma passagemCollectors.teeing(c1, c2, merger)
Tornar o resultado não modificávelenvolva em collectingAndThen(c, Collections::unmodifiableList)

Um exemplo prático: cada collector em um único conjunto de dados

O programa abaixo cria uma lista de registros Person e executa cada formato de collector sobre ela.

java— editable, runs on the server

O que observar na execução:

  • O toMap(Person::name, Person::age) sem proteção no final lançou IllegalStateException porque dois Persons compartilham o nome "Alice". A correção padrão é um terceiro argumento: um BinaryOperator<V> que diz como mesclar valores quando as chaves colidem. Escolha o merger que corresponda à sua semântica (manter o primeiro, manter o último, somar, concatenar) — é o que a chamada anterior ageByName fez com (a, b) -> a.
  • groupingBy(Person::role) produziu um Map<String, List<Person>> gratuitamente. Substituir o downstream padrão toList() por counting(), summingInt(...), averagingDouble(...) ou maxBy(...) transformou o resultado por grupo de "uma lista" em um único número — mesma forma de pipeline, receita diferente no slot downstream.
  • mapping(Person::name, toList()) é a resposta para "quero agrupar por função, mas meus grupos devem conter apenas nomes, não Persons inteiros." Pré-projetar downstream é quase sempre mais limpo do que coletar registros inteiros e depois mapear os valores.
  • partitioningBy retornou as chaves true e false mesmo quando uma metade poderia ter ficado vazia. Essa previsibilidade é sua razão de ser em comparação com groupingBy(predicado).
  • teeing coletou min e max em uma única passagem, depois entregou ambos os Optionals a um merger que construiu o registro Range. Sempre que você faria stream duas vezes para obter dois resumos, recorra a teeing.
  • collectingAndThen(toList(), Collections::unmodifiableList) é o truque clássico de finalizador; a mesma forma desencapsula um groupingBy(..., maxBy(...)) de Map<K, Optional<V>> para Map<K, V> quando você já provou que todo grupo é não vazio.

O que vem a seguir

Cada collector e intermediário da parte até agora roda sequencialmente por padrão — um elemento por vez, na ordem de encontro, na thread chamante. O próximo capítulo, Java Parallel Streams, apresenta o agendamento alternativo — parallelStream() e stream().parallel() — o que é seguro colocar dentro de um pipeline paralelo, o que não é (mutação de estado compartilhado, forEach sensível à ordem, reduce não associativo), e como saber se o paralelismo realmente ajuda ou torna o programa mais lento.

Prática

Prática
`people.stream().collect(Collectors.toMap(Person::name, Person::age))` lança `IllegalStateException` quando dois `Person`s compartilham um nome. Qual correção corresponde à intenção de 'somar suas idades quando os nomes colidem'?
`people.stream().collect(Collectors.toMap(Person::name, Person::age))` lança `IllegalStateException` quando dois `Person`s compartilham um nome. Qual correção corresponde à intenção de 'somar suas idades quando os nomes colidem'?
Was this page helpful?