Java
Tutorial Java: Agrupación y partición
Aprende a usar groupingBy y colectores downstream en Java Streams para agrupar y analizar datos de forma eficiente y avanzada.
Aprende Java y certifícategroupingBy() 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()
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.
Ejercicios de esta lección Agrupación y partición
Evalúa tus conocimientos de esta lección Agrupación y partición con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- 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.