Agrupando Dados com Collectors.groupingBy em Java
Uma das operações de coleta mais poderosas da Stream API é o agrupamento de dados. Para quem está familiarizado com SQL, o coletor Collectors.groupingBy()
é o análogo funcional da cláusula GROUP BY
. Ele permite segmentar os elementos de um stream em um Map
com base em um critério específico e, opcionalmente, realizar agregações dentro de cada grupo.
Para os exemplos a seguir, vamos utilizar um record
para representar um telefone:
record Phone(String name, String company, int price) {}
Agrupamento Simples
A forma mais básica de groupingBy
aceita uma função classificadora (classifier function
). Esta função é aplicada a cada elemento do stream, e o valor que ela retorna é usado como a chave no Map
resultante. Por padrão, os valores associados a cada chave são os elementos do stream, coletados em uma List
.
Vamos agrupar uma lista de telefones por fabricante (company
):
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
record Phone(String name, String company, int price) {}
public class GroupingExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(
new Phone("iPhone 15", "Apple", 999),
new Phone("Pixel 8", "Google", 799),
new Phone("iPhone 14", "Apple", 899),
new Phone("Galaxy S24", "Samsung", 899),
new Phone("Galaxy Z Fold5", "Samsung", 1799)
);
Map<String, List<Phone>> phonesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::company)
);
for (Map.Entry<String, List<Phone>> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey());
for (Phone phone : item.getValue()) {
System.out.println(" - " + phone.name());
}
System.out.println();
}
}
}
A expressão Phone::company
é a função classificadora. O resultado é um Map<String, List<Phone>>
.
Saída:
Google - Pixel 8 Apple - iPhone 15 - iPhone 14 Samsung - Galaxy S24 - Galaxy Z Fold5
Coletores Aninhados: Agregando Dados nos Grupos
Frequentemente, não queremos apenas a lista de objetos, mas sim realizar uma agregação dentro de cada grupo (contar, somar, etc.). Para isso, groupingBy
aceita um segundo argumento: um coletor aninhado (downstream collector
).
Contando Elementos em Grupos: Collectors.counting()
Para saber quantos telefones cada empresa possui na lista (análogo a COUNT(*)
em SQL).
// ... (imports e record Phone)
public class CountingExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(/* ... mesma lista de telefones ... */);
Map<String, Long> phonesCount = phoneStream.collect(
Collectors.groupingBy(Phone::company, Collectors.counting())
);
for (Map.Entry<String, Long> item : phonesCount.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue());
}
}
}
Saída:
Google - 1 Apple - 2 Samsung - 2
Somando Valores em Grupos: Collectors.summingInt()
Para calcular o valor total do estoque por empresa (análogo a SUM(price)
).
// ... (imports e record Phone)
public class SummingExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(/* ... mesma lista de telefones ... */);
Map<String, Integer> totalValueByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::company, Collectors.summingInt(Phone::price))
);
for (Map.Entry<String, Integer> item : totalValueByCompany.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue());
}
}
}
Saída:
Google - 799 Apple - 1898 Samsung - 2698
Encontrando Mínimos e Máximos: minBy()
e maxBy()
Para encontrar o telefone mais barato de cada empresa, usamos minBy
, que requer um Comparator
.
// ... (imports e record Phone)
import java.util.Comparator;
import java.util.Optional;
public class MinByExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(/* ... mesma lista de telefones ... */);
Map<String, Optional<Phone>> cheapestPhoneByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::company,
Collectors.minBy(Comparator.comparing(Phone::price)))
);
for (Map.Entry<String, Optional<Phone>> item : cheapestPhoneByCompany.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue().get().name());
}
}
}
Saída:
Google - Pixel 8 Apple - iPhone 14 Samsung - Galaxy S24
Obtendo Estatísticas Completas: summarizingInt()
O coletor summarizingInt()
calcula a contagem, soma, mínimo, máximo e a média de uma só vez.
// ... (imports e record Phone)
import java.util.IntSummaryStatistics;
public class SummarizingExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(/* ... mesma lista de telefones ... */);
Map<String, IntSummaryStatistics> priceSummary = phoneStream.collect(
Collectors.groupingBy(Phone::company,
Collectors.summarizingInt(Phone::price))
);
for (Map.Entry<String, IntSummaryStatistics> item : priceSummary.entrySet()) {
System.out.println(item.getKey() + " - Média: " + item.getValue().getAverage());
}
}
}
Saída:
Google - Média: 799.0 Apple - Média: 949.0 Samsung - Média: 1349.0
Mapeando os Valores do Grupo: mapping()
Para agrupar por empresa, mas coletar apenas os nomes dos telefones em uma lista.
// ... (imports e record Phone)
public class MappingExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(/* ... mesma lista de telefones ... */);
Map<String, List<String>> phoneNamesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::company,
Collectors.mapping(Phone::name, Collectors.toList()))
);
for (Map.Entry<String, List<String>> item : phoneNamesByCompany.entrySet()) {
System.out.println(item.getKey());
for (String name : item.getValue()) {
System.out.println(" - " + name);
}
}
}
}
Saída:
Google - Pixel 8 Apple - iPhone 15 - iPhone 14 Samsung - Galaxy S24 - Galaxy Z Fold5
Particionando: Um Caso Especial de Agrupamento
O Collectors.partitioningBy()
divide o stream em exatamente duas coleções (verdadeiro/falso) com base em um Predicate
.
// ... (imports e record Phone)
public class PartitioningExample {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(/* ... mesma lista de telefones ... */);
Map<Boolean, List<Phone>> phonesByPrice = phoneStream.collect(
Collectors.partitioningBy(p -> p.price() > 900)
);
for (Map.Entry<Boolean, List<Phone>> item : phonesByPrice.entrySet()) {
System.out.println("Preço > 900: " + item.getKey());
for (Phone phone : item.getValue()) {
System.out.println(" - " + phone.name());
}
System.out.println();
}
}
}
Resumo
Collectors.groupingBy(classifier)
agrupa elementos de um stream em umMap
, onde a chave é o resultado do classificador e o valor é umaList
dos elementos.groupingBy
pode aceitar um segundo coletor (um coletor aninhado) para processar os valores de cada grupo (ex:counting()
,summingInt()
,mapping()
).Collectors.partitioningBy(predicate)
é uma forma otimizada de agrupamento que sempre divide o stream em duas partições (verdadeiro/falso).