Con los streams en Java se pueden hacer en tres líneas los resúmenes que en SQL harías con GROUP BY: ventas por ciudad, cuántos pedidos por cliente, el promedio por categoría. Todo con los colectores de Collectors.
También te puede interesar
Los datos
Una lista de ventas, cada una con ciudad, producto e importe. Usaremos estos mismos datos en todos los ejemplos:
import java.util.*;
import static java.util.stream.Collectors.*;
record Venta(String ciudad, String producto, double importe) {}
final List<Venta> VENTAS = List.of(
new Venta("CDMX", "café", 150), new Venta("Guadalajara", "té", 90),
new Venta("CDMX", "pan", 45), new Venta("Monterrey", "café", 210),
new Venta("Guadalajara", "café", 130), new Venta("CDMX", "café", 95));
void main() {
// Cuántas ventas por ciudad (TreeMap: ordenadas por nombre)
Map<String, Long> porCiudad = VENTAS.stream()
.collect(groupingBy(Venta::ciudad, TreeMap::new, counting()));
IO.println("ventas por ciudad: " + porCiudad);
// Importe total por producto
Map<String, Double> totalProducto = VENTAS.stream()
.collect(groupingBy(Venta::producto, TreeMap::new, summingDouble(Venta::importe)));
IO.println("total por producto: " + totalProducto);
// Promedio por ciudad
VENTAS.stream()
.collect(groupingBy(Venta::ciudad, TreeMap::new, averagingDouble(Venta::importe)))
.forEach((c, p) -> IO.println("promedio %s: %.2f".formatted(c, p)));
// Ventas grandes y chicas
Map<Boolean, List<String>> partes = VENTAS.stream()
.collect(partitioningBy(v -> v.importe() >= 100, mapping(Venta::producto, toList())));
IO.println("de 100 o más: " + partes.get(true) + "; menores: " + partes.get(false));
// La venta más grande de cada ciudad
VENTAS.stream()
.collect(toMap(Venta::ciudad, v -> v, (a, b) -> a.importe() >= b.importe() ? a : b, TreeMap::new))
.forEach((c, v) -> IO.println("mayor en " + c + ": " + v.producto() + " $" + v.importe()));
}salidaventas por ciudad: {CDMX=3, Guadalajara=2, Monterrey=1}
total por producto: {café=585.0, pan=45.0, té=90.0}
promedio CDMX: 96.67
promedio Guadalajara: 110.00
promedio Monterrey: 210.00
de 100 o más: [café, café, café]; menores: [té, pan, café]
mayor en CDMX: café $150.0
mayor en Guadalajara: café $130.0
mayor en Monterrey: café $210.0Qué hace cada colector
groupingBy(clave, colector)reparte los elementos en grupos según la clave y aplica el segundo colector a cada grupo. Sin el segundo, cada grupo es una lista. ConTreeMap::newen medio, el resultado sale ordenado por clave (unHashMapno garantiza ningún orden).counting(),summingDouble()yaveragingDouble()resumen cada grupo en un número.partitioningByes ungroupingBycon dos grupos,trueyfalse;mappingtransforma los elementos antes de juntarlos.toMap(clave, valor, combinar, mapa)crea un mapa y necesita saber qué hacer cuando dos elementos tienen la misma clave: aquí, quedarse con el de mayor importe. Sin esa función, una clave repetida lanzaIllegalStateException.
Para importes reales usa BigDecimal en vez de double; con reducing(BigDecimal.ZERO, Venta::importe, BigDecimal::add) se suman igual.
Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.