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:
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);[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);[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 unPredicate.map(función)convierte cada elemento en otro. Es unaFunction: crea unStringa partir de unOrder.sorted()ordena según el orden natural, ysorted(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,sortedson operaciones intermedias: cada una devuelve un stream nuevo.toList,count,collect,forEachson 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áscollect(Collectors.toList()). Hace lo mismo, pero no promete exactamente qué tipo de lista devuelve. Si necesitas unArrayListque puedas cambiar, escribecollect(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
- Cinco precios. stream() solo prepara el camino: todavía no ha pasado nada.
- 12 no pasa p >= 50 y se queda fuera. Nunca llega a map.
- 75 pasa filter, map lo convierte en el texto «75 €», toList() lo reúne.
- 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
- 130 y 58 pasan. La lista prices no cambia.
- 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());El stream está listo, todavía no pasó nada por él.filter 12filter 75map 75filter 40[150]- La primera línea de la salida se imprime antes de que un solo elemento haya pasado por el stream.
map 75llega justo después defilter 75: los elementos no esperan a que todos pasen porfilter.- 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);Suma de los números pares: 6mapToInt(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);Pedidos de desde 50 €: 3Ingresos: 312.40 €Media: 62.48 €Mayor: 120.00 €count()da la cantidad de elementos que llegaron al final. El tipo eslong.average()ymax()no devuelven undoubledirectamente. Si no hay pedidos, no hay media. Por eso devuelven «quizá un número», yorElse(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));[2, 3, 4, 5][Diego, María, Ana]Mesa 1Mesa 2Mesa 3distinct()quita los repetidos. Compara conequals.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 consorted, eso es un «top 3».IntStream.range(1, 4)da los números 1, 2, 3. El final no se incluye, como enfor (int i = 1; i < 4; i++). ConrangeClosed(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);distinct() deja café, té, vino, jugo. sorted() los ordena alfabéticamente: café, jugo, té, vino. limit(3) se queda con los tres primeros, y map los pasa a mayúsculas.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"));Mayor: Diegofalseno existe ese pedidoifPresent(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"));Apodo: Mimisin apodoMá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());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 calculax.orElseGet(() -> ...)llama a la función solo si no hay valor. Notas la diferencia si el valor de reserva es costoso de calcular.average()deIntStreamyDoubleStreamdevuelve unOptionalDouble, ymax()deIntStreamdevuelve unOptionalInt. Son lo mismo para los tipos primitivos:getAsDouble(),orElse(0).Optionales para el resultado de un método. No lo uses como campo de una clase ni como parámetro.anyMatch,allMatchynoneMatchresponden contrueofalsesi alguno, todos o ninguno de los elementos cumplen la condición.
Pruébalo
Encuentra el error
Elige la línea con el error.
Un stream es una cinta transportadora, no una lista. count() la pone en marcha, y se consume. El segundo intento, en la línea 4, detiene el programa:
Cantidad: 2Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closedSi necesitas dos resultados, reúne los precios filtrados una vez con toList() y usa la lista: expensive.size() y expensive. O crea un stream nuevo para cada resultado con prices.stream().
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);{Granada=1, Madrid=2, Valencia=2}Granada: 120.00 €Madrid: 115.50 €Valencia: 76.90 €Ana, Diego, Elena, Jorge, Maríacollect 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 unTreeMap, cuenta cada grupo.summingDouble(Order::getTotal)en lugar decounting()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 unMap<String, List<Order>>: para cada ciudad, una lista de sus pedidos. - Si quieres una lista de nombres en lugar de pedidos,
mappingtransforma 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);{Granada=[Diego], Madrid=[María, Elena], Valencia=[Jorge, Ana]}Collectors.partitioningBy(condición)divide en dos grupos:trueyfalse.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
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 €.
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().
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).
4.50 5.75 6.00 3.00 5.50Media: 4.95Máxima: 6.00Excelentes: 34. 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}]+").
Café con leche, café sin azúcar. ¡Té con miel!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
groupingByyCollectors.averagingDouble. - Pedidos grandes y pequeños. Con
anyMatch, comprueba si hay un pedido de más de 1000 €, y conallMatchsi todos son de al menos 10 €.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Listas · HashMap y HashSet · Clases anidadas · Lambdas
- Continúa en: record, switch y patrones · Tests con JUnit · Bases de datos · Reflexión y anotaciones
- Ejercicios sobre el tema: Java moderno