CodeGym /Cursos /JAVA 25 SELF /Agregação avançada: agrupamentos aninhados

Agregação avançada: agrupamentos aninhados

JAVA 25 SELF
Nível 31 , Lição 3
Disponível

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
groupingBy(Employee::getDepartment)
Map<String, List<Employee>>
groupingBy(Employee::getDepartment, averagingInt(...))
Map<String, Double>
groupingBy(Employee::getDepartment, groupingBy(...))
Map<String, Map<String, List<Employee>>>
groupingBy(..., mapping(..., toList()))
Map<..., List<...>>
groupingBy(..., groupingBy(..., mapping(..., toList())))
Map<..., Map<..., List<...>>>

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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION