1. groupingBy aninhado: sintaxe e princípio de funcionamento
Na prática, raramente basta agrupar dados por um único critério. Por exemplo, se você tem uma lista de funcionários da empresa, muitas vezes quer saber não apenas quantas pessoas há em cada departamento, mas quantas há em cada departamento para cada cargo. Ou, se você tem uma base de estudantes — quantos estudantes há em cada curso para cada especialidade.
Agrupamentos aninhados permitem construir essas “mapas dentro de mapas” — a estrutura “departamento → cargo → lista de funcionários”, ou “curso → especialidade → lista de estudantes”.
Sem a Stream API, tais tarefas seriam resolvidas com vários loops aninhados e construção manual de Map dentro de Map. Com streams e coletores, isso se faz em uma ou duas linhas.
Um agrupamento aninhado é quando, como segundo argumento do método Collectors.groupingBy, você passa outro coletor, por exemplo, mais um groupingBy. Como resultado, obtém-se um mapa em que o valor para cada chave é outro mapa.
Modelo geral
Map<Chave1, Map<Chave2, List<T>>> result =
stream.collect(Collectors.groupingBy(
objeto -> chave1,
Collectors.groupingBy(objeto -> chave2)
));
Exemplo: funcionários por departamento e cargo
Suponha que temos a classe:
class Employee {
private String name;
private String department;
private String position;
private int salary;
// ... construtores, getters, toString
}
Lista de funcionários:
List<Employee> employees = List.of(
new Employee("Ivan", "TI", "Desenvolvedor", 120_000),
new Employee("Maria", "TI", "Testador", 90_000),
new Employee("Pyotr", "HR", "Gerente", 80_000),
new Employee("Olga", "TI", "Desenvolvedor", 130_000),
new Employee("Svetlana", "HR", "Recrutador", 70_000)
);
Agrupando por departamento e cargo:
Map<String, Map<String, List<Employee>>> grouped = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)
));
O que obtivemos?
Um mapa em que a chave é o departamento e o valor é um mapa (chave — cargo, valor — lista de funcionários).
Visualização da estrutura
TI:
Desenvolvedor: [Ivan, Olga]
Testador: [Maria]
HR:
Gerente: [Pyotr]
Recrutador: [Svetlana]
2. Agrupamento com agregação: combinando groupingBy e agregadores
Agrupamentos aninhados não se limitam a coletar listas! Podemos agregar os dados imediatamente dentro de cada subgrupo.
Exemplo: salário máximo por departamento
Map<String, Optional<Employee>> maxSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
));
Aqui, para cada departamento, obtemos o funcionário com o salário máximo (o resultado vem envolto em Optional, porque um departamento pode estar vazio).
Agrupamento aninhado + agregação
Suponha que queiramos saber o salário máximo para cada cargo em cada departamento:
Map<String, Map<String, Optional<Employee>>> maxSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
)
));
O que isso significa?
- Para cada departamento — um mapa de cargos.
- Para cada cargo — o funcionário com o salário máximo (ou um Optional vazio, se não houver ninguém).
3. Agrupamento com transformação: mapping dentro de groupingBy
Às vezes, não é necessário apenas agrupar objetos, mas obter somente certos campos dentro dos grupos.
Exemplo: nomes dos funcionários por departamento
Map<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
Resultado:
TI: [Ivan, Maria, Olga]
HR: [Pyotr, Svetlana]
mapping aninhado
É possível combinar mapping com groupingBy aninhado:
Map<String, Map<String, List<String>>> namesByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.mapping(Employee::getName, Collectors.toList())
)
));
Resultado:
TI:
Desenvolvedor: [Ivan, Olga]
Testador: [Maria]
HR:
Gerente: [Pyotr]
Recrutador: [Svetlana]
4. Agrupamento + agregação de dados numéricos
Muitas vezes é necessário não apenas agrupar, mas calcular soma, média ou quantidade por grupos.
Exemplo: salário médio por departamento
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Agregação aninhada
Salário médio por cargo em cada departamento:
Map<String, Map<String, Double>> avgSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.averagingInt(Employee::getSalary)
)
));
5. partitioningBy + agregação
Às vezes é conveniente dividir a coleção em dois grupos por um predicado booleano e, dentro deles, também agregar.
Exemplo: quantos funcionários com salário acima de 100_000 em cada departamento
Map<String, Map<Boolean, Long>> countByDeptAndSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.partitioningBy(
e -> e.getSalary() > 100_000,
Collectors.counting()
)
));
Resultado:
Para cada departamento — um mapa: true/false → quantidade de funcionários.
6. Tarefas práticas: aplicando agrupamentos aninhados
Tarefa 1. Estudantes por curso e especialidade
class Student {
private String name;
private int course;
private String speciality;
private double grade;
// ... getters, construtor
}
List<Student> students = ... // suponha que já existe
Map<Integer, Map<String, List<Student>>> byCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(Student::getSpeciality)
));
Tarefa 2. Nota média por curso
Map<Integer, Double> avgGradeByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.averagingDouble(Student::getGrade)
));
Tarefa 3. Somente nomes por grupos
Map<Integer, Map<String, List<String>>> namesByCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(
Student::getSpeciality,
Collectors.mapping(Student::getName, Collectors.toList())
)
));
7. Nuances úteis
Como ler e extrair dados de Maps aninhados
Trabalhar com mapas aninhados pode ser incomum no começo. Eis um exemplo básico:
for (var deptEntry : grouped.entrySet()) {
String dept = deptEntry.getKey();
Map<String, List<Employee>> byPosition = deptEntry.getValue();
System.out.println("Departamento: " + dept);
for (var posEntry : byPosition.entrySet()) {
String pos = posEntry.getKey();
List<Employee> emps = posEntry.getValue();
System.out.println(" Cargo: " + pos + " -> " + emps);
}
}
Esquema do agrupamento aninhado
Map<Departamento, Map<Cargo, List<Employee>>>
│
├── "TI"
│ ├── "Desenvolvedor" → [Ivan, Olga]
│ └── "Testador" → [Maria]
└── "HR"
├── "Gerente" → [Pyotr]
└── "Recrutador" → [Svetlana]
Tabela: o que você obtém após diferentes combinações
| Coletor | Resultado |
|---|---|
|
|
|
|
|
|
|
|
|
|
8. Erros comuns ao trabalhar com agrupamentos aninhados
Erro nº 1: compreensão incorreta da estrutura de Maps aninhados.
Após agrupamentos aninhados, é fácil se confundir sobre o que exatamente está no valor de cada mapa. Sempre observe a assinatura do resultado — a IDE mostrará o tipo. Se não tiver certeza, imprima o resultado na tela com System.out.println(grouped) ou use o depurador.
Erro nº 2: NullPointerException ao extrair dados.
Se a chave não existir (por exemplo, não há funcionários de um determinado cargo em um departamento), Map.get(key) retornará null. Verifique a presença da chave com containsKey ou, a partir do Java 8, use Map.getOrDefault, e a partir do Java 9 — Map.ofNullable e métodos de Optional.
Erro nº 3: aninhamentos complexos demais.
Se o agrupamento ficar profundo demais (3–4 níveis de aninhamento), talvez valha repensar a estrutura dos dados ou dividir a tarefa em etapas menores.
Erro nº 4: agregação no nível errado.
Às vezes, por engano, colocam o coletor agregador (averagingInt, counting) fora do groupingBy necessário — e obtêm um resultado inesperado. Sempre preste muita atenção ao posicionamento dos parênteses!
Erro nº 5: tentar modificar elementos dentro de collect.
Não modifique coleções ou objetos de origem durante o agrupamento — isso pode levar a bugs difíceis de detectar.
GO TO FULL VERSION