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.
GO TO FULL VERSION