Mira la lección en vídeo
Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.
Desbloquear Plan PlusgroupingBy() básico: Map<K,List>
El método groupingBy()
es uno de los colectores más útiles y versátiles de la API Stream de Java. Este colector permite agrupar elementos de un stream según una función de clasificación, creando un mapa donde las claves son los resultados de aplicar dicha función a cada elemento, y los valores son listas que contienen todos los elementos que comparten la misma clave.
La forma básica de groupingBy()
produce un Map<K,List<T>>
donde:
K
es el tipo de la clave (resultado de la función clasificadora)List<T>
es una lista de elementos originales que comparten esa clave
Sintaxis básica
Map<K, List<T>> result = stream.collect(Collectors.groupingBy(classifier));
Donde classifier
es una función que extrae la clave de agrupación de cada elemento del stream.
Casos de uso comunes
Veamos algunos ejemplos prácticos para entender mejor cómo funciona este colector:
Agrupar palabras por su primera letra
List<String> palabras = List.of("manzana", "pera", "melón", "plátano", "mandarina", "piña");
Map<Character, List<String>> porPrimeraLetra = palabras.stream()
.collect(Collectors.groupingBy(palabra -> palabra.charAt(0)));
System.out.println(porPrimeraLetra);
// Output: {p=[pera, plátano, piña], m=[manzana, melón, mandarina]}
En este ejemplo, la función clasificadora extrae el primer carácter de cada palabra, agrupando todas las palabras que comienzan con la misma letra.
Agrupar personas por edad
class Persona {
private String nombre;
private int edad;
// Constructor, getters y setters
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
@Override
public String toString() {
return nombre;
}
}
List<Persona> personas = List.of(
new Persona("Ana", 25),
new Persona("Carlos", 30),
new Persona("Elena", 25),
new Persona("David", 30),
new Persona("Beatriz", 22)
);
Map<Integer, List<Persona>> porEdad = personas.stream()
.collect(Collectors.groupingBy(Persona::getEdad));
porEdad.forEach((edad, grupo) -> {
System.out.println("Edad " + edad + ": " + grupo);
});
// Output:
// Edad 22: [Beatriz]
// Edad 25: [Ana, Elena]
// Edad 30: [Carlos, David]
Aquí utilizamos una referencia a método (Persona::getEdad
) como clasificador, lo que hace el código más limpio y legible.
Uso con objetos más complejos
El poder de groupingBy()
se hace más evidente cuando trabajamos con estructuras de datos más complejas:
class Producto {
private String nombre;
private String categoria;
private double precio;
// Constructor y getters
public Producto(String nombre, String categoria, double precio) {
this.nombre = nombre;
this.categoria = categoria;
this.precio = precio;
}
public String getNombre() { return nombre; }
public String getCategoria() { return categoria; }
public double getPrecio() { return precio; }
@Override
public String toString() {
return nombre + " (" + precio + "€)";
}
}
List<Producto> productos = List.of(
new Producto("Laptop", "Electrónica", 899.99),
new Producto("Smartphone", "Electrónica", 499.99),
new Producto("Camiseta", "Ropa", 19.99),
new Producto("Pantalón", "Ropa", 39.99),
new Producto("Zapatillas", "Calzado", 59.99)
);
Map<String, List<Producto>> porCategoria = productos.stream()
.collect(Collectors.groupingBy(Producto::getCategoria));
porCategoria.forEach((categoria, listaProductos) -> {
System.out.println(categoria + ":");
listaProductos.forEach(producto -> System.out.println(" - " + producto));
});
Este código produce una salida como:
Electrónica:
- Laptop (899.99€)
- Smartphone (499.99€)
Ropa:
- Camiseta (19.99€)
- Pantalón (39.99€)
Calzado:
- Zapatillas (59.99€)
Funciones clasificadoras personalizadas
Podemos crear clasificadores más complejos para agrupar según criterios específicos:
// Agrupar productos por rango de precio
Map<String, List<Producto>> porRangoPrecio = productos.stream()
.collect(Collectors.groupingBy(producto -> {
double precio = producto.getPrecio();
if (precio < 50) return "Económico";
else if (precio < 500) return "Medio";
else return "Premium";
}));
porRangoPrecio.forEach((rango, lista) -> {
System.out.println("Rango " + rango + ":");
lista.forEach(p -> System.out.println(" - " + p.getNombre() + " (" + p.getPrecio() + "€)"));
});
Trabajando con el mapa resultante
Una vez que tenemos nuestro Map<K,List<T>>
, podemos manipularlo como cualquier otro mapa:
// Obtener todos los productos de una categoría específica
List<Producto> electronicos = porCategoria.getOrDefault("Electrónica", Collections.emptyList());
// Verificar si existe una categoría
boolean existeCategoria = porCategoria.containsKey("Juguetes");
// Iterar sobre las entradas del mapa
porCategoria.entrySet().forEach(entry -> {
String categoria = entry.getKey();
List<Producto> productosCategoria = entry.getValue();
System.out.println("La categoría " + categoria + " tiene " + productosCategoria.size() + " productos");
});
Consideraciones de rendimiento
El colector groupingBy()
es eficiente para la mayoría de los casos de uso, pero hay algunas consideraciones a tener en cuenta:
- Para colecciones muy grandes, el mapa resultante podría ocupar mucha memoria.
- Si la función clasificadora es computacionalmente costosa, el rendimiento puede verse afectado.
- Por defecto,
groupingBy()
utilizaHashMap
como implementación del mapa resultante, lo que proporciona un buen equilibrio entre rendimiento y uso de memoria.
Ejemplo práctico: análisis de datos
Veamos un ejemplo más completo que muestra cómo groupingBy()
puede ser útil para el análisis de datos:
class Venta {
private String producto;
private String region;
private int cantidad;
private LocalDate fecha;
// Constructor y getters
public Venta(String producto, String region, int cantidad, LocalDate fecha) {
this.producto = producto;
this.region = region;
this.cantidad = cantidad;
this.fecha = fecha;
}
public String getProducto() { return producto; }
public String getRegion() { return region; }
public int getCantidad() { return cantidad; }
public LocalDate getFecha() { return fecha; }
public int getAño() { return fecha.getYear(); }
public int getMes() { return fecha.getMonthValue(); }
}
List<Venta> ventas = List.of(
new Venta("Laptop", "Norte", 5, LocalDate.of(2023, 1, 15)),
new Venta("Smartphone", "Sur", 10, LocalDate.of(2023, 1, 20)),
new Venta("Laptop", "Este", 3, LocalDate.of(2023, 2, 5)),
new Venta("Tablet", "Oeste", 7, LocalDate.of(2023, 2, 10)),
new Venta("Smartphone", "Norte", 8, LocalDate.of(2023, 3, 2))
);
// Agrupar ventas por región
Map<String, List<Venta>> ventasPorRegion = ventas.stream()
.collect(Collectors.groupingBy(Venta::getRegion));
// Agrupar ventas por mes
Map<Integer, List<Venta>> ventasPorMes = ventas.stream()
.collect(Collectors.groupingBy(Venta::getMes));
// Agrupar ventas por producto
Map<String, List<Venta>> ventasPorProducto = ventas.stream()
.collect(Collectors.groupingBy(Venta::getProducto));
Este tipo de agrupación es extremadamente útil para generar informes y análisis de datos, permitiéndonos ver rápidamente cómo se distribuyen las ventas por diferentes dimensiones.
El método groupingBy()
básico es solo el comienzo. En las siguientes secciones veremos cómo podemos combinar este colector con otros para realizar operaciones más avanzadas sobre los grupos, como contar elementos, calcular sumas o crear agrupaciones anidadas.
counting(), mapping(), summingInt()
Guarda tu progreso
Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.
Más de 25.000 desarrolladores ya confían en CertiDevs
Cuando trabajamos con el método groupingBy()
en Java, podemos ir más allá de simplemente agrupar elementos en listas. La API Stream nos permite aplicar colectores downstream para realizar operaciones adicionales sobre cada grupo. Estos colectores nos ayudan a transformar, agregar o resumir los elementos dentro de cada grupo de manera eficiente.
El colector counting()
El colector counting()
nos permite contar el número de elementos en cada grupo, en lugar de mantener la lista completa de elementos. Esto es especialmente útil cuando solo nos interesa la cantidad de elementos y no los elementos en sí.
Map<String, Long> conteoProductosPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
Collectors.counting()
));
conteoProductosPorCategoria.forEach((categoria, cantidad) -> {
System.out.println(categoria + ": " + cantidad + " productos");
});
// Output:
// Electrónica: 2 productos
// Ropa: 2 productos
// Calzado: 1 producto
Observa cómo el tipo del valor en el mapa resultante cambia de List<Producto>
a Long
, reflejando el conteo de elementos en cada grupo.
El colector mapping()
El colector mapping()
nos permite transformar los elementos de cada grupo antes de recopilarlos. Recibe dos parámetros:
- Una función de mapeo que transforma cada elemento
- Un colector que determina cómo se recopilarán los elementos transformados
// Obtener solo los nombres de los productos por categoría
Map<String, List<String>> nombresPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
Collectors.mapping(
Producto::getNombre,
Collectors.toList()
)
));
nombresPorCategoria.forEach((categoria, nombres) -> {
System.out.println(categoria + ": " + nombres);
});
// Output:
// Electrónica: [Laptop, Smartphone]
// Ropa: [Camiseta, Pantalón]
// Calzado: [Zapatillas]
En este ejemplo, en lugar de almacenar objetos Producto
completos, solo almacenamos sus nombres como strings.
También podemos combinar mapping()
con otros colectores para obtener resultados más específicos:
// Obtener nombres únicos de productos por categoría
Map<String, Set<String>> nombresUnicosPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
Collectors.mapping(
Producto::getNombre,
Collectors.toSet()
)
));
El colector summingInt()
El colector summingInt()
nos permite calcular la suma de un valor numérico para cada grupo. Es particularmente útil para calcular totales o agregados.
// Calcular el total de unidades vendidas por región
List<Venta> ventas = List.of(
new Venta("Laptop", "Norte", 5),
new Venta("Smartphone", "Sur", 10),
new Venta("Laptop", "Norte", 3),
new Venta("Tablet", "Oeste", 7),
new Venta("Smartphone", "Norte", 8)
);
Map<String, Integer> unidadesPorRegion = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getRegion,
Collectors.summingInt(Venta::getCantidad)
));
unidadesPorRegion.forEach((region, total) -> {
System.out.println("Región " + region + ": " + total + " unidades");
});
// Output:
// Región Norte: 16 unidades
// Región Sur: 10 unidades
// Región Oeste: 7 unidades
Existen variantes para otros tipos numéricos como summingLong()
y summingDouble()
:
// Calcular el valor total de ventas por categoría
class Producto {
private String nombre;
private String categoria;
private double precio;
private int unidadesVendidas;
// Constructor y getters
// ...
public double getValorTotal() {
return precio * unidadesVendidas;
}
}
Map<String, Double> valorVentasPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
Collectors.summingDouble(Producto::getValorTotal)
));
Combinando colectores para análisis más complejos
Podemos combinar estos colectores para realizar análisis más sofisticados:
// Calcular precio promedio por categoría
Map<String, Double> precioPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
Collectors.averagingDouble(Producto::getPrecio)
));
// Encontrar el producto más caro por categoría
Map<String, Optional<Producto>> productoMasCaroPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
Collectors.maxBy(Comparator.comparing(Producto::getPrecio))
));
Ejemplo práctico: análisis de ventas
Veamos un ejemplo más completo que combina varios colectores para analizar datos de ventas:
class Venta {
private String producto;
private String categoria;
private String region;
private int cantidad;
private double precioUnitario;
// Constructor y getters
public Venta(String producto, String categoria, String region, int cantidad, double precioUnitario) {
this.producto = producto;
this.categoria = categoria;
this.region = region;
this.cantidad = cantidad;
this.precioUnitario = precioUnitario;
}
public String getProducto() { return producto; }
public String getCategoria() { return categoria; }
public String getRegion() { return region; }
public int getCantidad() { return cantidad; }
public double getPrecioUnitario() { return precioUnitario; }
public double getImporteTotal() { return cantidad * precioUnitario; }
}
List<Venta> ventas = List.of(
new Venta("Laptop XPS", "Electrónica", "Norte", 2, 1299.99),
new Venta("Smartphone Galaxy", "Electrónica", "Sur", 5, 799.99),
new Venta("Auriculares Bluetooth", "Accesorios", "Norte", 10, 89.99),
new Venta("Monitor 4K", "Electrónica", "Este", 3, 349.99),
new Venta("Teclado Mecánico", "Accesorios", "Oeste", 7, 129.99),
new Venta("Ratón Inalámbrico", "Accesorios", "Norte", 15, 49.99)
);
// 1. Número de ventas por categoría
Map<String, Long> ventasPorCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getCategoria,
Collectors.counting()
));
// 2. Total de unidades vendidas por categoría
Map<String, Integer> unidadesPorCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getCategoria,
Collectors.summingInt(Venta::getCantidad)
));
// 3. Importe total de ventas por categoría
Map<String, Double> importePorCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getCategoria,
Collectors.summingDouble(Venta::getImporteTotal)
));
// 4. Productos vendidos por categoría
Map<String, Set<String>> productosPorCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getCategoria,
Collectors.mapping(
Venta::getProducto,
Collectors.toSet()
)
));
// Mostrar resultados
System.out.println("Análisis de ventas por categoría:");
ventasPorCategoria.forEach((cat, count) -> {
System.out.println("\nCategoría: " + cat);
System.out.println(" Número de ventas: " + count);
System.out.println(" Unidades vendidas: " + unidadesPorCategoria.get(cat));
System.out.println(" Importe total: " + importePorCategoria.get(cat) + "€");
System.out.println(" Productos: " + productosPorCategoria.get(cat));
});
Este código genera un informe completo de ventas por categoría, mostrando diferentes métricas para cada grupo.
Colectores personalizados
Además de los colectores predefinidos, podemos crear nuestros propios colectores para operaciones específicas:
// Colector personalizado para calcular la mediana de precios
Collector<Producto, ?, Double> medianaPrecios = Collector.of(
ArrayList::new, // supplier
List::add, // accumulator
(left, right) -> { // combiner
left.addAll(right);
return left;
},
list -> { // finisher
list.sort(Comparator.comparing(Producto::getPrecio));
int size = list.size();
if (size == 0) return 0.0;
if (size % 2 == 0) {
return (list.get(size/2 - 1).getPrecio() + list.get(size/2).getPrecio()) / 2;
} else {
return list.get(size/2).getPrecio();
}
}
);
Map<String, Double> medianaPrecioPorCategoria = productos.stream()
.collect(Collectors.groupingBy(
Producto::getCategoria,
medianaPrecios
));
Los colectores downstream como counting()
, mapping()
y summingInt()
amplían significativamente las capacidades de agrupación en Java, permitiéndonos realizar análisis de datos complejos de manera concisa y expresiva. Estas herramientas son fundamentales para trabajar con colecciones de datos en aplicaciones modernas de Java.
groupingBy() anidado: Map<K1,Map<K2,List>>
La agrupación anidada es una técnica avanzada que nos permite clasificar elementos en múltiples niveles jerárquicos. Mientras que el groupingBy()
básico crea un mapa con un solo nivel de agrupación, el groupingBy()
anidado nos permite crear estructuras de datos más complejas con múltiples niveles de clasificación.
Estructura de agrupación anidada
Cuando utilizamos groupingBy()
anidado, obtenemos un mapa donde:
- Las claves primarias (
K1
) representan el primer criterio de agrupación - Los valores son mapas secundarios donde:
- Las claves secundarias (
K2
) representan el segundo criterio de agrupación - Los valores finales son listas de elementos originales (
List<T>
)
La estructura resultante es Map<K1, Map<K2, List<T>>>
, lo que nos permite navegar por los datos en diferentes niveles de granularidad.
Sintaxis básica
Map<K1, Map<K2, List<T>>> result = stream.collect(
Collectors.groupingBy(
classifier1, // Función para la clave primaria
Collectors.groupingBy(classifier2) // Colector downstream para la clave secundaria
)
);
Ejemplo práctico: ventas por región y categoría
Veamos un ejemplo concreto para entender mejor cómo funciona:
class Venta {
private String producto;
private String categoria;
private String region;
private double importe;
// Constructor y getters
public Venta(String producto, String categoria, String region, double importe) {
this.producto = producto;
this.categoria = categoria;
this.region = region;
this.importe = importe;
}
public String getProducto() { return producto; }
public String getCategoria() { return categoria; }
public String getRegion() { return region; }
public double getImporte() { return importe; }
@Override
public String toString() {
return producto + " (" + importe + "€)";
}
}
List<Venta> ventas = List.of(
new Venta("Laptop", "Electrónica", "Norte", 1200.0),
new Venta("Smartphone", "Electrónica", "Sur", 800.0),
new Venta("Tablet", "Electrónica", "Norte", 500.0),
new Venta("Camiseta", "Ropa", "Este", 25.0),
new Venta("Pantalón", "Ropa", "Norte", 45.0),
new Venta("Zapatillas", "Calzado", "Sur", 60.0),
new Venta("Reloj", "Accesorios", "Este", 120.0)
);
// Agrupar ventas por región y luego por categoría
Map<String, Map<String, List<Venta>>> ventasPorRegionYCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getRegion, // Clasificador primario: región
Collectors.groupingBy(Venta::getCategoria) // Clasificador secundario: categoría
));
Este código agrupa primero las ventas por región y luego, dentro de cada región, las agrupa por categoría. El resultado es una estructura jerárquica que nos permite analizar las ventas desde diferentes perspectivas.
Navegando por la estructura anidada
Para acceder a los datos en una estructura anidada, necesitamos navegar por los diferentes niveles:
// Imprimir la estructura completa
ventasPorRegionYCategoria.forEach((region, porCategoria) -> {
System.out.println("Región: " + region);
porCategoria.forEach((categoria, ventasCategoria) -> {
System.out.println(" Categoría: " + categoria);
ventasCategoria.forEach(venta -> {
System.out.println(" - " + venta);
});
});
System.out.println();
});
La salida sería algo como:
Región: Norte
Categoría: Electrónica
- Laptop (1200.0€)
- Tablet (500.0€)
Categoría: Ropa
- Pantalón (45.0€)
Región: Sur
Categoría: Electrónica
- Smartphone (800.0€)
Categoría: Calzado
- Zapatillas (60.0€)
Región: Este
Categoría: Ropa
- Camiseta (25.0€)
Categoría: Accesorios
- Reloj (120.0€)
Acceso directo a grupos específicos
Podemos acceder directamente a un grupo específico utilizando las claves correspondientes:
// Obtener todas las ventas de electrónica en la región Norte
List<Venta> electronicaNorte = ventasPorRegionYCategoria
.getOrDefault("Norte", Collections.emptyMap())
.getOrDefault("Electrónica", Collections.emptyList());
System.out.println("Ventas de electrónica en el Norte:");
electronicaNorte.forEach(venta -> System.out.println("- " + venta));
Combinando con otros colectores downstream
Al igual que con el groupingBy()
simple, podemos combinar el groupingBy()
anidado con otros colectores para realizar operaciones más avanzadas:
// Calcular el importe total de ventas por región y categoría
Map<String, Map<String, Double>> importeTotalPorRegionYCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getRegion,
Collectors.groupingBy(
Venta::getCategoria,
Collectors.summingDouble(Venta::getImporte)
)
));
// Imprimir los resultados
importeTotalPorRegionYCategoria.forEach((region, porCategoria) -> {
System.out.println("Región: " + region);
porCategoria.forEach((categoria, importeTotal) -> {
System.out.println(" " + categoria + ": " + importeTotal + "€");
});
System.out.println();
});
Este código calcula el importe total de ventas para cada combinación de región y categoría, produciendo una salida como:
Región: Norte
Electrónica: 1700.0€
Ropa: 45.0€
Región: Sur
Electrónica: 800.0€
Calzado: 60.0€
Región: Este
Ropa: 25.0€
Accesorios: 120.0€
Más de dos niveles de agrupación
Podemos extender este patrón para crear agrupaciones con más de dos niveles:
// Agregar año a la clase Venta
class Venta {
// ... otros campos y métodos
private int año;
public Venta(String producto, String categoria, String region, double importe, int año) {
// ... inicialización de otros campos
this.año = año;
}
public int getAño() { return año; }
}
// Agrupar por año, región y categoría
Map<Integer, Map<String, Map<String, List<Venta>>>> ventasPorAñoRegionCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getAño,
Collectors.groupingBy(
Venta::getRegion,
Collectors.groupingBy(Venta::getCategoria)
)
));
Esta estructura de tres niveles nos permite analizar las ventas por año, luego por región y finalmente por categoría.
Caso práctico: análisis de empleados
Veamos un ejemplo más completo con datos de empleados:
class Empleado {
private String nombre;
private String departamento;
private String oficina;
private String nivel;
private double salario;
// Constructor y getters
public Empleado(String nombre, String departamento, String oficina,
String nivel, double salario) {
this.nombre = nombre;
this.departamento = departamento;
this.oficina = oficina;
this.nivel = nivel;
this.salario = salario;
}
public String getNombre() { return nombre; }
public String getDepartamento() { return departamento; }
public String getOficina() { return oficina; }
public String getNivel() { return nivel; }
public double getSalario() { return salario; }
@Override
public String toString() {
return nombre + " (" + nivel + ", " + salario + "€)";
}
}
List<Empleado> empleados = List.of(
new Empleado("Ana García", "Desarrollo", "Madrid", "Senior", 65000),
new Empleado("Carlos López", "Desarrollo", "Barcelona", "Junior", 35000),
new Empleado("Elena Martín", "Marketing", "Madrid", "Senior", 60000),
new Empleado("David Sánchez", "Ventas", "Valencia", "Mid", 45000),
new Empleado("Beatriz Ruiz", "Desarrollo", "Madrid", "Mid", 48000),
new Empleado("Fernando Torres", "Marketing", "Barcelona", "Junior", 32000),
new Empleado("Lucía Díaz", "Ventas", "Madrid", "Senior", 70000)
);
// Agrupar empleados por oficina y departamento
Map<String, Map<String, List<Empleado>>> porOficinaDepartamento = empleados.stream()
.collect(Collectors.groupingBy(
Empleado::getOficina,
Collectors.groupingBy(Empleado::getDepartamento)
));
// Calcular salario promedio por oficina y nivel
Map<String, Map<String, Double>> salarioPromedioPorOficinaNivel = empleados.stream()
.collect(Collectors.groupingBy(
Empleado::getOficina,
Collectors.groupingBy(
Empleado::getNivel,
Collectors.averagingDouble(Empleado::getSalario)
)
));
// Contar empleados por departamento y nivel
Map<String, Map<String, Long>> conteoEmpleadosPorDepartamentoNivel = empleados.stream()
.collect(Collectors.groupingBy(
Empleado::getDepartamento,
Collectors.groupingBy(
Empleado::getNivel,
Collectors.counting()
)
));
Transformando los resultados con mapping()
Podemos combinar groupingBy()
anidado con mapping()
para transformar los elementos en cada grupo:
// Obtener nombres de empleados por oficina y departamento
Map<String, Map<String, List<String>>> nombresPorOficinaDepartamento = empleados.stream()
.collect(Collectors.groupingBy(
Empleado::getOficina,
Collectors.groupingBy(
Empleado::getDepartamento,
Collectors.mapping(
Empleado::getNombre,
Collectors.toList()
)
)
));
// Imprimir los resultados
nombresPorOficinaDepartamento.forEach((oficina, porDepartamento) -> {
System.out.println("Oficina: " + oficina);
porDepartamento.forEach((departamento, nombres) -> {
System.out.println(" " + departamento + ": " + nombres);
});
System.out.println();
});
Consideraciones de rendimiento y legibilidad
Las agrupaciones anidadas son potentes, pero hay que tener en cuenta algunas consideraciones:
- Complejidad: A medida que aumentamos los niveles de anidación, la estructura se vuelve más compleja y difícil de manejar.
- Rendimiento: Las agrupaciones múltiples pueden requerir más recursos de memoria y procesamiento.
- Legibilidad: El código con múltiples niveles de agrupación puede ser difícil de leer y mantener.
Para mejorar la legibilidad, podemos definir tipos específicos para nuestras estructuras anidadas:
// Definir tipos para mejorar la legibilidad
typedef Map<String, List<Venta>> VentasPorCategoria;
typedef Map<String, VentasPorCategoria> VentasPorRegionCategoria;
// Usar los tipos definidos
VentasPorRegionCategoria ventasPorRegionYCategoria = ventas.stream()
.collect(Collectors.groupingBy(
Venta::getRegion,
Collectors.groupingBy(Venta::getCategoria)
));
Ejemplo final: análisis de datos de estudiantes
Veamos un último ejemplo completo que muestra el poder de las agrupaciones anidadas:
class Estudiante {
private String nombre;
private String facultad;
private String carrera;
private int curso;
private double notaMedia;
// Constructor y getters
public Estudiante(String nombre, String facultad, String carrera,
int curso, double notaMedia) {
this.nombre = nombre;
this.facultad = facultad;
this.carrera = carrera;
this.curso = curso;
this.notaMedia = notaMedia;
}
public String getNombre() { return nombre; }
public String getFacultad() { return facultad; }
public String getCarrera() { return carrera; }
public int getCurso() { return curso; }
public double getNotaMedia() { return notaMedia; }
}
List<Estudiante> estudiantes = List.of(
new Estudiante("Ana", "Ciencias", "Física", 2, 8.7),
new Estudiante("Carlos", "Ciencias", "Matemáticas", 3, 7.9),
new Estudiante("Elena", "Ingeniería", "Informática", 2, 9.1),
new Estudiante("David", "Ingeniería", "Informática", 4, 8.2),
new Estudiante("Beatriz", "Ciencias", "Física", 3, 8.5),
new Estudiante("Fernando", "Letras", "Historia", 2, 7.8),
new Estudiante("Lucía", "Letras", "Filosofía", 4, 9.3)
);
// Calcular nota media por facultad y carrera
Map<String, Map<String, Double>> notaMediaPorFacultadCarrera = estudiantes.stream()
.collect(Collectors.groupingBy(
Estudiante::getFacultad,
Collectors.groupingBy(
Estudiante::getCarrera,
Collectors.averagingDouble(Estudiante::getNotaMedia)
)
));
// Contar estudiantes por facultad, carrera y curso
Map<String, Map<String, Map<Integer, Long>>> conteoEstudiantesPorFacultadCarreraCurso =
estudiantes.stream()
.collect(Collectors.groupingBy(
Estudiante::getFacultad,
Collectors.groupingBy(
Estudiante::getCarrera,
Collectors.groupingBy(
Estudiante::getCurso,
Collectors.counting()
)
)
));
// Imprimir notas medias
System.out.println("Nota media por facultad y carrera:");
notaMediaPorFacultadCarrera.forEach((facultad, porCarrera) -> {
System.out.println(facultad + ":");
porCarrera.forEach((carrera, media) -> {
System.out.printf(" %s: %.2f\n", carrera, media);
});
});
Las agrupaciones anidadas son una herramienta fundamental para el análisis de datos en Java, permitiéndonos crear estructuras jerárquicas que facilitan la exploración y el análisis de información desde múltiples perspectivas. Aunque pueden resultar complejas, su poder y flexibilidad las convierten en una técnica esencial para cualquier desarrollador que trabaje con colecciones de datos.
Aprendizajes de esta lección de Java
- Comprender el funcionamiento básico del método groupingBy() para agrupar elementos en mapas.
- Aprender a utilizar colectores downstream como counting(), mapping() y summingInt() para realizar operaciones agregadas sobre grupos.
- Aplicar agrupaciones anidadas con groupingBy() para crear estructuras jerárquicas de datos.
- Manejar y navegar estructuras de datos complejas resultantes de agrupaciones múltiples.
- Evaluar consideraciones de rendimiento y legibilidad al trabajar con agrupaciones y colectores en Java.
Completa este curso de Java y certifícate
Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.
Asistente IA
Resuelve dudas al instante
Ejercicios
Practica con proyectos reales
Certificados
Valida tus conocimientos
Más de 25.000 desarrolladores ya se han certificado con CertiDevs