CodeGym /Cursos /JAVA 25 SELF /Coletores avançados

Coletores avançados

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

1. Introdução

Quando você trabalha com coleções via Stream API, muitas vezes é preciso não apenas agrupar os elementos, mas também já fazer algo com eles: transformar, filtrar, coletar em outro tipo de coleção. Para isso, o Java tem coletores downstream — coletores aninhados que são aplicados a cada grupo ou parte dos dados.

O que é um coletor downstream?

É um coletor aplicado ao resultado de um agrupamento ou particionamento. Por exemplo, você pode agrupar estudantes por curso com groupingBy e, dentro de cada grupo, coletar apenas os nomes ou apenas os de excelência (por exemplo, com o limiar 4.5 de GPA).

Exemplos

mapping

Permite transformar os elementos do grupo antes da coleta.

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • Agrupamos os estudantes por curso.
  • Para cada grupo coletamos apenas os nomes (e não os objetos inteiros).

filtering

Permite filtrar os elementos dentro do grupo (por exemplo, manter GPA >= 4.5).

Map<Integer, List<Student>> honorsByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
    ));

Em cada grupo mantemos apenas os de excelência.

flatMapping

Permite “desdobrar” coleções aninhadas dentro dos grupos.

Map<String, Set<String>> tagsByAuthor = books.stream()
    .collect(Collectors.groupingBy(
        Book::getAuthor,
        Collectors.flatMapping(
            book -> book.getTags().stream(),
            Collectors.toSet()
        )
    ));

Para cada autor, coletamos as tags únicas de todos os seus livros.

partitioningBy com downstream

Funciona de forma semelhante ao groupingBy, mas divide em dois grupos por uma condição booleana.

Map<Boolean, List<String>> namesByPassed = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGpa() >= 3.0,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

Separamos os estudantes entre aprovados e reprovados e, dentro de cada grupo, apenas os nomes.

2. teeing: agregação simultânea com dois coletores

Às vezes é preciso calcular vários agregados ao mesmo tempo no stream: por exemplo, soma e média, ou mínimo e máximo. Para isso, no Java 12+ apareceu o coletor teeing.

Como funciona o teeing?

Você fornece dois coletores e uma função que combina seus resultados.

Sintaxe:

Collectors.teeing(collector1, collector2, (result1, result2) -> ...)

Exemplos

Mínimo + máximo

Optional<MinMax> minMax = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
    ));

Encontramos mínimo e máximo ao mesmo tempo.

Soma + média

Result result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.averagingInt(Integer::intValue),
        (sum, avg) -> new Result(sum, avg)
    ));

Obtemos um objeto com a soma e a média.

Exemplo: relatório de salários

SalaryStats stats = employees.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Employee::getSalary),
        Collectors.averagingInt(Employee::getSalary),
        SalaryStats::new
    ));

Em SalaryStats armazenamos tanto a soma quanto a média.

3. toUnmodifiableList/Set/Map e collectingAndThen para “congelar”

Nas versões modernas do Java surgiram coleções que não podem ser alteradas após a criação — imutáveis (unmodifiable). Isso é útil para APIs em que é importante que o resultado não possa ser alterado por engano.

toUnmodifiableList/Set/Map

  • Retornam uma coleção imutável.
  • Qualquer tentativa de adicionar/remover um elemento lançará UnsupportedOperationException.

Exemplos:

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
    .collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));

collectingAndThen

Permite aplicar uma função ao resultado do coletor — por exemplo, “congelar” a coleção.

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

Primeiro coletamos em uma lista comum e depois a tornamos imutável.

Exemplo com Set:

Set<String> tags = books.stream()
    .flatMap(book -> book.getTags().stream())
    .collect(Collectors.collectingAndThen(
        Collectors.toSet(),
        Set::copyOf // Java 10+
    ));

4. Cenários de pipeline

Relatórios e estatísticas por segmentos

Com coletores avançados é possível montar relatórios e estatísticas complexos “em uma única linha”.

Exemplo: salário médio por departamento

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

Exemplo: top 3 produtos mais caros por categoria

Map<String, List<Product>> top3ByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(Product::getPrice).reversed())
                        .limit(3)
                        .toList()
        )
    ));

Resultado imutável como contrato de API

Se o seu método retorna uma coleção que não pode ser modificada, isso evita erros acidentais e torna a API mais confiável.

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}

O usuário não conseguirá fazer getTags().add("nova tag") — isso lançará uma exceção.

Exemplo: relatório com vários agregados (teeing)

public SalaryReport getSalaryReport(List<Employee> employees) {
    return employees.stream()
        .collect(Collectors.teeing(
            Collectors.averagingInt(Employee::getSalary),
            Collectors.summingInt(Employee::getSalary),
            SalaryReport::new
        ));
}

Em SalaryReport armazenamos tanto a média quanto a soma dos salários.

5. Erros comuns e nuances

Erro nº 1: Esqueceu da imutabilidade. Se você retorna uma lista comum, ela pode ser modificada. Use toUnmodifiableList/Set/Map ou collectingAndThen para “congelar” o resultado.

Erro nº 2: Coletor downstream incorreto. Se precisar transformar elementos dentro do grupo — use mapping; se filtrar — filtering; se “desdobrar” coleções aninhadas — flatMapping.

Erro nº 3: UnsupportedOperationException. Ocorre ao tentar modificar uma coleção coletada via toUnmodifiableList/Set/Map ou “congelada” via collectingAndThen.

Erro nº 4: Perda de unicidade/colisões de chaves. toUnmodifiableSet exige elementos únicos, e toUnmodifiableMap — chaves únicas; caso contrário, você receberá uma exceção durante a coleta.

1
Pesquisa/teste
Agrupamento e agregação, nível 31, lição 4
Indisponível
Agrupamento e agregação
Stream API: agrupamento e agregação
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION