Ir al contenido
wannacode

Streams

En resumen: Un stream procesa una colección como una serie de pasos: filter criba, map transforma, sorted ordena y una operación terminal como toList da el resultado.

Problema

Un informe con un bucle

Ahora los pedidos también tienen una ciudad:

Order
class Order {
private String customer;
private String city;
private double total;
public Order(String customer, String city, double total) {
this.customer = customer;
this.city = city;
this.total = total;
}
public String getCustomer() {
return customer;
}
public String getCity() {
return city;
}
public double getTotal() {
return total;
}
}
List<Order> orders = List.of(
new Order("María", "Madrid", 75.50),
new Order("Jorge", "Valencia", 12.90),
new Order("Elena", "Madrid", 40.00),
new Order("Diego", "Granada", 120.00),
new Order("Ana", "Valencia", 64.00));

Quieres los clientes con pedidos de al menos 50 €, en orden alfabético:

List<String> result = new ArrayList<>();
for (Order o : orders) {
if (o.getTotal() >= 50) {
result.add(o.getCustomer());
}
}
Collections.sort(result);
System.out.println(result);
Salida
[Ana, Diego, María]

Ocho líneas, y la pregunta es una sola. El código dice cómo: crea una lista vacía, recorre, comprueba, añade, ordena. Para entender qué hace, tienes que ejecutarlo en tu cabeza. Añade «solo los tres primeros» o «sin repetidos», y el bucle crecerá.

La idea

Un stream: una serie de pasos

El mismo informe, escrito como pasos:

List<String> result = orders.stream()
.filter(o -> o.getTotal() >= 50)
.map(Order::getCustomer)
.sorted()
.toList();
System.out.println(result);
Salida
[Ana, Diego, María]

Se lee de arriba abajo: toma los pedidos, quédate con los de al menos 50 €, toma el cliente, ordena, reúne en una lista.

orders.stream() crea un stream a partir de la lista. Un stream no guarda datos. Es una cinta transportadora por la que los elementos avanzan de uno en uno. Cada paso recibe una lambda de la lección anterior:

  • filter(condición) deja pasar solo los elementos para los que la condición es verdadera. Es un Predicate.
  • map(función) convierte cada elemento en otro. Es una Function: crea un String a partir de un Order.
  • sorted() ordena según el orden natural, y sorted(comparador) según tu regla.
  • toList() reúne en una lista nueva todo lo que llegó al final.

La lista orders no cambia. El stream solo la lee.

Más precisamente, para curiosos
  • filter, map, sorted son operaciones intermedias: cada una devuelve un stream nuevo. toList, count, collect, forEach son terminales: piden un resultado y ponen en marcha la cinta.
  • toList() es de Java 16 y devuelve una lista que no se puede cambiar. En código más antiguo verás collect(Collectors.toList()). Hace lo mismo, pero no promete exactamente qué tipo de lista devuelve. Si necesitas un ArrayList que puedas cambiar, escribe collect(Collectors.toCollection(ArrayList::new)).
  • También puedes crear un stream a partir de un array: Arrays.stream(prices), y a partir de valores sueltos: Stream.of("café", "té").

Míralo

Qué pasa el filtro

List<Integer> prices = List.of(12, 75, 40, 130, 58);
List<String> labels = prices.stream()
.filter(p -> p >= 50)
.map(p -> p + " €")
.toList();
System.out.println(labels);
Paso a paso, en palabras
  1. Cinco precios. stream() solo prepara el camino: todavía no ha pasado nada.
  2. 12 no pasa p >= 50 y se queda fuera. Nunca llega a map.
  3. 75 pasa filter, map lo convierte en el texto «75 €», toList() lo reúne.
  4. 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
  5. 130 y 58 pasan. La lista prices no cambia.
  6. El resultado es una lista nueva con tres cadenas.

Los cinco precios avanzan por la cinta de uno en uno. El 12 llega a filter y se queda fuera. El 75 pasa, map lo convierte en el texto «75 €» y entra en la lista. Después el 40 se queda fuera, y el 130 y el 58 pasan.

Cada elemento hace todo el recorrido antes de que empiece el siguiente. Y nada empieza hasta que la operación terminal toList() pide un resultado.

Más precisamente, para curiosos

Esto se llama evaluación perezosa. Las operaciones intermedias solo anotan lo que hay que hacer. Míralo con un print en cada paso:

Stream<Integer> stream = List.of(12, 75, 40).stream()
.filter(p -> {
System.out.println("filter " + p);
return p >= 50;
})
.map(p -> {
System.out.println("map " + p);
return p * 2;
});
System.out.println("El stream está listo, todavía no pasó nada por él.");
System.out.println(stream.toList());
Salida
El stream está listo, todavía no pasó nada por él.
filter 12
filter 75
map 75
filter 40
[150]
  • La primera línea de la salida se imprime antes de que un solo elemento haya pasado por el stream.
  • map 75 llega justo después de filter 75: los elementos no esperan a que todos pasen por filter.
  • Por eso limit(3) detiene la cinta en cuanto tres elementos llegan al final. Los demás ni siquiera se comprueban.
  • parallelStream() procesa los elementos en varios hilos a la vez. Ayuda con muchos datos y cálculos pesados. Con listas pequeñas es más lento, y las lambdas no deben tocar datos compartidos. Los hilos están en el módulo «Programas en el mundo real».

La idea

Suma, cantidad, media

Un stream puede terminar con un número en lugar de una lista. La suma de los números pares:

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream().filter(n -> n % 2 == 0).mapToInt(n -> n).sum();
System.out.println("Suma de los números pares: " + sum);
Salida
Suma de los números pares: 6
mapToInt(n -> n) crea un stream de int a partir de un Stream<Integer>. Se llama IntStream y tiene sum(), average(), max() y min(). Para double existen mapToDouble y DoubleStream.

Lo mismo para los pedidos:

long big = orders.stream().filter(o -> o.getTotal() >= 50).count();
double revenue = orders.stream().mapToDouble(Order::getTotal).sum();
double average = orders.stream().mapToDouble(Order::getTotal).average().orElse(0);
double maxTotal = orders.stream().mapToDouble(Order::getTotal).max().orElse(0);
System.out.println("Pedidos de desde 50 €: " + big);
System.out.printf("Ingresos: %.2f €%n", revenue);
System.out.printf("Media: %.2f €%n", average);
System.out.printf("Mayor: %.2f €%n", maxTotal);
Salida
Pedidos de desde 50 €: 3
Ingresos: 312.40 €
Media: 62.48 €
Mayor: 120.00 €
  • count() da la cantidad de elementos que llegaron al final. El tipo es long.
  • average() y max() no devuelven un double directamente. Si no hay pedidos, no hay media. Por eso devuelven «quizá un número», y orElse(0) dice qué debe ser si no lo hay. Esa es la siguiente idea.

La idea

Ordenar, recortar, sin repetidos

List<Integer> ratings = List.of(5, 3, 5, 4, 2, 4, 5);
System.out.println(ratings.stream().distinct().sorted().toList());
List<String> top = orders.stream()
.sorted(Comparator.comparing(Order::getTotal).reversed())
.limit(3)
.map(Order::getCustomer)
.toList();
System.out.println(top);
IntStream.range(1, 4).forEach(i -> System.out.println("Mesa " + i));
Salida
[2, 3, 4, 5]
[Diego, María, Ana]
Mesa 1
Mesa 2
Mesa 3
  • distinct() quita los repetidos. Compara con equals.
  • sorted(Comparator.comparing(Order::getTotal).reversed()) ordena empezando por el pedido más grande, con un comparador de la lección anterior.
  • limit(3) deja pasar solo los tres primeros. Junto con sorted, eso es un «top 3».
  • IntStream.range(1, 4) da los números 1, 2, 3. El final no se incluye, como en for (int i = 1; i < 4; i++). Con rangeClosed(1, 4) sí.

Pruébalo

¿Qué llega al final?

¿Qué imprimirá el programa?

List<String> drinks = List.of("café", "té", "café", "vino", "té", "jugo");
List<String> result = drinks.stream()
.distinct()
.sorted()
.limit(3)
.map(String::toUpperCase)
.toList();
System.out.println(result);
Respuesta

La idea

Optional: un resultado que puede faltar

¿Cuál es el pedido más grande? Si la lista está vacía, no hay respuesta. El método podría devolver null, pero entonces quien llama tiene que acordarse de comprobarlo. Si se le olvida, el programa se detiene con una NullPointerException en algún sitio más adelante.

Por eso max, min y findFirst devuelven un Optional: una caja que o bien contiene un valor, o bien está vacía.

Optional<Order> biggest = orders.stream().max(Comparator.comparing(Order::getTotal));
biggest.ifPresent(o -> System.out.println("Mayor: " + o.getCustomer()));
Optional<Order> huge = orders.stream().filter(o -> o.getTotal() > 500).findFirst();
System.out.println(huge.isPresent());
System.out.println(huge.map(Order::getCustomer).orElse("no existe ese pedido"));
Salida
Mayor: Diego
false
no existe ese pedido
  • ifPresent(acción) hace la acción solo si hay un valor.
  • isPresent() te dice si lo hay.
  • orElse(reserva) da el valor o el valor de reserva.
  • map(función) transforma el valor, si lo hay. Una caja vacía sigue vacía.

Optional también ayuda con tus propios métodos que devuelven null. Optional.ofNullable mete en la caja lo que recibe: un valor o nada.

static String nickname(String user) {
// Devuelve el apodo, o null si la persona no tiene.
return user.equals("maria") ? "Mimi" : null;
}
Optional<String> maria = Optional.ofNullable(nickname("maria"));
maria.ifPresent(name -> System.out.println("Apodo: " + name));
Optional<String> jorge = Optional.ofNullable(nickname("jorge"));
System.out.println(jorge.orElse("sin apodo"));
Salida
Apodo: Mimi
sin apodo
Más precisamente, para curiosos
  • get() da el valor, pero si la caja está vacía, lanza una excepción:
Optional<Order> huge = orders.stream().filter(o -> o.getTotal() > 500).findFirst();
System.out.println(huge.get().getCustomer());
Error
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:143)

orElseThrow() hace lo mismo, pero su nombre dice honestamente lo que va a pasar. Úsalo si una caja vacía es un error.

  • orElse(x) siempre calcula x. orElseGet(() -> ...) llama a la función solo si no hay valor. Notas la diferencia si el valor de reserva es costoso de calcular.
  • average() de IntStream y DoubleStream devuelve un OptionalDouble, y max() de IntStream devuelve un OptionalInt. Son lo mismo para los tipos primitivos: getAsDouble(), orElse(0).
  • Optional es para el resultado de un método. No lo uses como campo de una clase ni como parámetro.
  • anyMatch, allMatch y noneMatch responden con true o false si alguno, todos o ninguno de los elementos cumplen la condición.

Pruébalo

Encuentra el error

El programa debería imprimir cuántos precios son caros, y los propios precios. Sale la primera línea y luego el programa se detiene. ¿En qué línea está el error?

Elige la línea con el error.

La idea

Agrupar y unir

¿Cuántos pedidos hay de cada ciudad, y cuáles son los ingresos?

Map<String, Long> countByCity = orders.stream()
.collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.counting()));
System.out.println(countByCity);
Map<String, Double> revenueByCity = orders.stream()
.collect(Collectors.groupingBy(Order::getCity, TreeMap::new, Collectors.summingDouble(Order::getTotal)));
revenueByCity.forEach((city, sum) -> System.out.printf("%s: %.2f €%n", city, sum));
String customers = orders.stream()
.map(Order::getCustomer)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(customers);
Salida
{Granada=1, Madrid=2, Valencia=2}
Granada: 120.00 €
Madrid: 115.50 €
Valencia: 76.90 €
Ana, Diego, Elena, Jorge, María

collect reúne el stream según una regla de la clase Collectors:

  • groupingBy(Order::getCity, TreeMap::new, Collectors.counting()): agrupa por ciudad, pon los grupos en un TreeMap, cuenta cada grupo.
  • summingDouble(Order::getTotal) en lugar de counting() suma en lugar de contar.
  • joining(", ") une las cadenas en una sola, con una coma entre ellas.

TreeMap::new es una referencia a un constructor. Dice qué tipo de mapa debe crear groupingBy. Sin ella, el resultado es un HashMap, y el orden de las ciudades en él no está garantizado.

Más precisamente, para curiosos
  • Solo con la función, groupingBy(Order::getCity), el resultado es un Map<String, List<Order>>: para cada ciudad, una lista de sus pedidos.
  • Si quieres una lista de nombres en lugar de pedidos, mapping transforma cada elemento antes de que entre en el grupo:
Map<String, List<String>> namesByCity = orders.stream()
.collect(Collectors.groupingBy(Order::getCity, TreeMap::new,
Collectors.mapping(Order::getCustomer, Collectors.toList())));
System.out.println(namesByCity);
Salida
{Granada=[Diego], Madrid=[María, Elena], Valencia=[Jorge, Ana]}
  • Collectors.partitioningBy(condición) divide en dos grupos: true y false. Collectors.toMap(clave, valor) crea un mapa con un valor por clave y lanza una excepción si una clave se repite.

Ejercicio

Informes con streams

Resuelve las tareas en tu editor. Importa java.util.stream.Collectors y, para los arrays, java.util.Arrays.

1. Ventas por categoría. Una clase Sale con un producto, una categoría, una cantidad y un precio unitario, y un método getRevenue() (cantidad por precio). Suma los ingresos por categoría en un TreeMap e imprime los ingresos totales. Las ventas: café en grano, bebidas, 12 a 18,50 €; té, bebidas, 30 a 4,20 €; chocolate, dulces, 40 a 2,80 €; galletas, dulces, 25 a 3,10 €; miel, comida, 8 a 12 €; otra vez café en grano, 5 a 18,50 €.

Salida
Bebidas: 440.50 €
Comida: 96.00 €
Dulces: 189.50 €
Total: 726.00 €

2. Los 3 mejores productos. Con las mismas ventas, encuentra los tres productos con más ingresos. El café está en dos ventas, así que primero agrupa por producto y suma, y luego ordena las entradas del mapa por valor. Pista: map.entrySet().stream() y Map.Entry.<String, Double>comparingByValue().reversed().

Salida
Café en grano: 314.50 €
Té: 126.00 €
Chocolate: 112.00 €

3. Nota media. Lee una línea de notas separadas por espacios. Imprime la media, la más alta y cuántas son excelentes (5,50 o más). Pista: Arrays.stream(line.split(" ")).mapToDouble(Double::parseDouble).

Entrada
4.50 5.75 6.00 3.00 5.50
Salida
Media: 4.95
Máxima: 6.00
Excelentes: 3

4. Las palabras de un texto. Lee una línea de texto e imprime sus palabras sin repetidos, en orden alfabético, en minúsculas y separadas por comas. Divide por todo lo que no sea una letra: split("[^\\p{L}]+").

Entrada
Café con leche, café sin azúcar. ¡Té con miel!
Salida
azúcar, café, con, leche, miel, sin, té

Más tareas

  • La palabra más larga. Con max(Comparator.comparing(String::length)), encuentra la palabra más larga de una frase. ¿Qué devolverá el programa para una línea vacía?
  • Media por grupo. Estudiantes con un nombre, un grupo y una nota: la nota media de cada grupo con groupingBy y Collectors.averagingDouble.
  • Pedidos grandes y pequeños. Con anyMatch, comprueba si hay un pedido de más de 1000 €, y con allMatch si todos son de al menos 10 €.

Test

Ponte a prueba

1. ¿Qué le pasa a la lista orders después de orders.stream().filter(o -> o.getTotal() > 50).toList()?
2. ¿Cuál de estas es una operación terminal que pone en marcha el stream?
3. orders.stream().max(...) devuelve un Optional<Order>. ¿Por qué no simplemente un Order?
4. ¿Por qué la agrupación va en un TreeMap y no es simplemente groupingBy(Order::getCity)?
5. Stream<Integer> s = list.stream(); s.count(); s.toList(); ¿Qué pasa?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·