Las lambdas y los streams son lo que más ha cambiado la cara del código Java en la última década. Esta lección enseña a leerlos y a escribirlos, y también —porque hace falta— cuándo un bucle de toda la vida es mejor idea.
Una lambda es un objeto
En la lección 6 apareció de pasada: cuando una interfaz tiene un solo método abstracto, se puede implementar con una flecha en vez de con una clase. Java trae ya escritas las cuatro formas que hacen falta casi siempre:
Predicate<String> esLargo = s -> s.length() > 5; // devuelve boolean Function<String,Integer> largo = String::length; // transforma Supplier<String> saludo = () -> "hola"; // produce, sin entrada Consumer<String> imprime = s -> IO.println(s); // consume, sin salida
salidaesLargo("mochila") = true
largo("uva") = 3
saludo() = hola
consumido
Ese String::length es una referencia a método: la forma corta de s -> s.length(). Hay cuatro variantes que conviene reconocer: Clase::metodoEstatico, objeto::metodo, Clase::metodoDeInstancia (el objeto es el argumento) y Clase::new.
Y se combinan entre sí:
esLargo.negate().test("mochila"); // false
largo.andThen(n -> n * 2).apply("uva"); // 6
salidacomponer: false 6
Un stream es una tubería
Tres partes, siempre en este orden: de dónde salen los datos, qué se les hace, y en qué acaban.
List<String> hay = ps.stream()
.filter(p -> p.stock() > 0) // intermedias: devuelven otro stream
.sorted(Comparator.comparingDouble(Producto::precio))
.map(Producto::nombre)
.toList(); // terminal: produce el resultado
salida[bufanda, gorra, camiseta, mochila]
filter y no llamas a toList, count o similar, no pasa absolutamente nada: es el error más común al empezar, y no da ningún aviso.Acabar en un número
ps.stream().filter(p -> p.stock() > 0).count(); ps.stream().mapToDouble(Producto::precio).sum(); ps.stream().mapToDouble(Producto::precio).average().orElse(0); ps.stream().max(Comparator.comparingDouble(Producto::precio)); ps.stream().mapToDouble(Producto::precio).summaryStatistics();
salidacuántos : 4
total : 91.15
media : 18.23
el más caro: mochila
resumen : DoubleSummaryStatistics{count=5, sum=91.150000, min=9.000000, average=18.230000, max=34.000000}
mapToDouble no es decorativo: convierte a un DoubleStream, que tiene sum, average y summaryStatistics y además evita empaquetar cada número en un objeto. Fíjate en que average() devuelve un Optional, porque la media de cero elementos no existe.
Agrupar
Map<String,List<String>> porCat = ps.stream()
.collect(Collectors.groupingBy(Producto::categoria,
Collectors.mapping(Producto::nombre, Collectors.toList())));
Map<String,Double> gasto = ps.stream()
.collect(Collectors.groupingBy(Producto::categoria,
Collectors.summingDouble(Producto::precio)));
salida{viaje=[mochila, neceser], ropa=[gorra, bufanda, camiseta]}
{viaje=49.75, ropa=41.4}
unidos: [gorra, bufanda, mochila, neceser, camiseta]
groupingBy es, con diferencia, lo que más justifica aprender streams: eso con bucles son quince líneas y un computeIfAbsent. El segundo argumento —el «colector de abajo»— decide qué se hace con cada grupo: juntarlos en una lista, sumarlos, contarlos, quedarse con el máximo.
Y joining con tres argumentos pega las piezas con separador, prefijo y sufijo de una vez.
| Quiero… | Colector |
|---|---|
| Una lista | .toList() |
| Un conjunto | Collectors.toSet() |
| Un mapa | Collectors.toMap(clave, valor) |
| Agrupar | Collectors.groupingBy(...) |
| Partir en dos por una condición | Collectors.partitioningBy(...) |
| Pegar textos | Collectors.joining(", ") |
| Contar por grupo | groupingBy(x, Collectors.counting()) |
Un stream se gasta
var s = ps.stream(); s.count(); s.count(); // <- aquí revienta
salidastream has already been operated upon or closed
No es una colección: es un recorrido de un solo uso. Si necesitas dos resultados, o guardas la lista y creas dos streams, o lo resuelves en una pasada con summaryStatistics o un teeing.
Cuándo no usarlos
Los streams no son más rápidos que un bucle; para colecciones pequeñas suelen ser algo más lentos, porque montan objetos. Lo que aportan es que el código diga qué se quiere en vez de cómo recorrerlo.
Dicho eso, hay tres casos en los que el bucle gana claro:
- Cuando hay que salir a la mitad. No hay
break; lo más parecido estakeWhile, y no siempre encaja. - Cuando el cuerpo lanza excepciones comprobadas. Las lambdas no las admiten, y acabas envolviendo todo en
trydentro delmap, que se lee fatal. - Cuando la tubería pasa de cinco o seis pasos. En ese punto un bucle con buenos nombres de variable suele ser más legible, y se puede depurar poniendo un punto de ruptura.
parallelStream(). Cambiar stream() por parallelStream() parece gratis y casi nunca lo es: reparte el trabajo en el grupo de hilos común de la JVM, que es compartido. Con pocos elementos sale más caro que la ganancia, y si la operación toca estado compartido, introduce una carrera. Antes de usarlo, mídelo.Lo esencial
| Para… | Se escribe |
|---|---|
| Quedarse con algunos | .filter(p -> ...) |
| Transformar cada uno | .map(T::campo) |
| Ordenar | .sorted(Comparator.comparing(...)) |
| Quitar repetidos | .distinct() |
| Los primeros n | .limit(n) |
| Acabar en lista | .toList() |
| Sumar o promediar | .mapToDouble(...).sum() |
| Agrupar | .collect(Collectors.groupingBy(...)) |
Todo el código de esta lección se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicarla; las salidas y los errores del compilador están copiados de esa ejecución.