CodeGym /Cursos /JAVA 25 SELF /Colectores avanzados

Colectores avanzados

JAVA 25 SELF
Nivel 31, Lección 4
Disponible

1. Introducción

Cuando trabajas con colecciones mediante Stream API, a menudo no basta con agrupar los elementos, sino que también necesitas hacer algo con ellos al instante: transformarlos, filtrarlos, recopilarlos en otro tipo de colección. Para ello, en Java existen los colectores downstream — colectores anidados que se aplican a cada grupo o parte de los datos.

¿Qué es un colector downstream?

Es un colector que se aplica al resultado de la agrupación o partición. Por ejemplo, puedes agrupar a los estudiantes por curso con groupingBy y, dentro de cada grupo, recopilar solo los nombres o solo a los sobresalientes (por ejemplo, con un umbral 4.5 de GPA).

Ejemplos

mapping

Permite transformar los elementos del grupo antes de recolectarlos.

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • Agrupamos a los estudiantes por curso.
  • Para cada grupo recolectamos solo los nombres (no los objetos completos).

filtering

Permite filtrar elementos dentro del grupo (por ejemplo, dejar GPA >= 4.5).

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

En cada grupo dejamos solo a los sobresalientes.

flatMapping

Permite «aplanar» colecciones anidadas dentro de los grupos.

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

Para cada autor recopilamos las etiquetas únicas de todos sus libros.

partitioningBy con downstream

Funciona de forma similar a groupingBy, pero divide en dos grupos según una condición booleana.

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

Dividimos a los estudiantes en aprobados y no aprobados; dentro de cada grupo, solo los nombres.

2. teeing: agregación simultánea con dos colectores

A veces necesitas calcular varios agregados del flujo a la vez: por ejemplo, la suma y la media, o el mínimo y el máximo. Para ello, en Java 12+ apareció el colector teeing.

¿Cómo funciona teeing?

Se pasan dos colectores y una función que combina sus resultados.

Sintaxis:

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

Ejemplos

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 simultáneamente el mínimo y el máximo.

Suma + media

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

Obtenemos un objeto con la suma y el valor medio.

Ejemplo: informe de salarios

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

En SalaryStats guardamos tanto la suma como la media.

3. toUnmodifiableList/Set/Map y collectingAndThen para la «congelación»

En las versiones modernas de Java aparecieron colecciones que no se pueden modificar tras su creación — inmutables (unmodifiable). Es útil para APIs donde es importante que el resultado no pueda cambiarse accidentalmente.

toUnmodifiableList/Set/Map

  • Devuelven una colección inmutable.
  • Cualquier intento de añadir/eliminar un elemento lanzará UnsupportedOperationException.

Ejemplos:

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 una función al resultado del colector — por ejemplo, «congelar» la colección.

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

Primero recolectamos en una lista normal y luego la hacemos inmutable.

Ejemplo con Set:

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

4. Casos de canalización

Informes y estadísticas por segmentos

Con los colectores avanzados se pueden construir informes y estadísticas complejos «en una sola línea».

Ejemplo: salario medio por departamento

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

Ejemplo: top 3 de los productos más caros por categoría

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 inmutable como contrato de la API

Si tu método devuelve una colección que no se puede modificar, esto protege contra errores accidentales y hace la API más robusta.

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

El usuario no podrá hacer getTags().add("nueva etiqueta") — se lanzará una excepción.

Ejemplo: informe con varios agregados (teeing)

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

En SalaryReport guardamos tanto el salario medio como el total.

5. Errores típicos y matices

Error n.º 1: Te olvidaste de la inmutabilidad. Si devuelves una lista normal, se puede modificar. Usa toUnmodifiableList/Set/Map o collectingAndThen para «congelar» el resultado.

Error n.º 2: Colector downstream incorrecto. Si necesitas transformar elementos dentro del grupo — usa mapping; si necesitas filtrar — filtering; si necesitas «aplanar» colecciones anidadas — flatMapping.

Error n.º 3: UnsupportedOperationException. Aparece al intentar modificar una colección recolectada mediante toUnmodifiableList/Set/Map o «congelada» mediante collectingAndThen.

Error n.º 4: Pérdida de unicidad/colisiones de claves. toUnmodifiableSet requiere elementos únicos y toUnmodifiableMap — claves únicas; de lo contrario, obtendrás una excepción durante la recolección.

1
Tarea
JAVA 25 SELF, nivel 31, lección 4
Bloqueada
Clasificación de aspirantes en la Academia de Grandes Logros 📜
Clasificación de aspirantes en la Academia de Grandes Logros 📜
1
Tarea
JAVA 25 SELF, nivel 31, lección 4
Bloqueada
Análisis exprés de la cartera financiera 💰
Análisis exprés de la cartera financiera 💰
1
Cuestionario/control
Agrupación y agregación, nivel 31, lección 4
No disponible
Agrupación y agregación
Stream API: agrupación y agregación
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION