Lambdas
En resumen: Una lambda es una función corta sin nombre que pasas como valor, por ejemplo para ordenar o filtrar; en Java implementa una interfaz funcional.
Problema
Seis líneas para una comparación
Hay que ordenar los pedidos por total. Ya sabes cómo desde el módulo «Colecciones»: sort con un Comparator, escrito como clase anónima.
class Order { private String customer; private double total;
public Order(String customer, double total) { this.customer = customer; this.total = total; }
public String getCustomer() { return customer; }
public double getTotal() { return total; }}List<Order> orders = new ArrayList<>(List.of( new Order("María", 75.50), new Order("Jorge", 12.90), new Order("Elena", 40.00)));
orders.sort(new Comparator<Order>() { @Override public int compare(Order a, Order b) { return Double.compare(a.getTotal(), b.getTotal()); }});
for (Order o : orders) { System.out.printf("%s: %.2f €%n", o.getCustomer(), o.getTotal());}Jorge: 12.90 €Elena: 40.00 €María: 75.50 €Funciona. Pero de las seis líneas de la clase anónima, solo una dice algo: Double.compare(a.getTotal(), b.getTotal()). El resto es envoltorio que escribes cada vez: new Comparator<Order>(), @Override, public int compare, llaves. Tú quieres pasar solo la regla.
La idea
Una lambda: solo la regla
El mismo orden, en una línea:
orders.sort((a, b) -> Double.compare(a.getTotal(), b.getTotal()));(a, b) -> Double.compare(a.getTotal(), b.getTotal()) se llama expresión lambda, o lambda para abreviar. A la izquierda de -> están los parámetros, a la derecha está el cuerpo. Es el método compare sin nombre, sin tipos y sin return.
Una lambda se puede escribir de tres formas:
- Un solo parámetro puede ir sin paréntesis:
o -> o.getTotal() > 50. Sin parámetros es() -> .... - Los tipos de los parámetros normalmente se omiten, porque el compilador los conoce por
sort. También puedes escribirlos:(Order a, Order b) -> .... - Un cuerpo de varias líneas va entre
{ }, y entoncesreturnes obligatorio.
Comparator<Order> byTotal = (a, b) -> Double.compare(a.getTotal(), b.getTotal());
Comparator<Order> byTotalLong = (Order a, Order b) -> { double diff = a.getTotal() - b.getTotal(); return diff < 0 ? -1 : diff > 0 ? 1 : 0;};
orders.sort(byTotalLong);for (Order o : orders) { System.out.printf("%s: %.2f €%n", o.getCustomer(), o.getTotal());}Jorge: 12.90 €Elena: 40.00 €María: 75.50 €La idea
Interfaces funcionales
¿Cómo sabe Java que (a, b) -> ... es el método compare? Por el tipo: sort espera un Comparator, y este tiene un método abstracto. La lambda se convierte en su cuerpo.
Una interfaz con exactamente un método abstracto se llama interfaz funcional. Puedes hacer la tuya:
interface PriceRule { double apply(double price);}PriceRule student = price -> price * 0.8;PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));System.out.println(minusFive.apply(50));40.045.0student es un objeto de tipo PriceRule. Lo llamas con el nombre del método de la interfaz: student.apply(50). Dos reglas de precio, y ni una sola clase que las implemente.PriceRule student = price -> price * 0.8;PriceRule minusFive = price -> price - 5;
System.out.println(student.apply(50));System.out.println(minusFive.apply(50));Paso a paso, en palabras
- La lambda es un objeto de tipo PriceRule, y student apunta a él. Su cuerpo todavía no se ejecuta.
- Una segunda regla, un segundo objeto. No se escribe ni una sola clase.
- student.apply(50) pasa 50 a la lambda: price es 50, y price * 0.8 da 40.0.
- minusFive.apply(50): el mismo número, otra regla, 45.0.
Más precisamente, para curiosos
- La anotación
@FunctionalInterfaceencima de la interfaz hace que el compilador compruebe que hay exactamente un método. Si añades un segundo método abstracto:
@FunctionalInterfaceinterface PriceRule { double apply(double price); String name();}Main.java:1: error: Unexpected @FunctionalInterface annotation@FunctionalInterface^ PriceRule is not a functional interface multiple non-overriding abstract methods found in interface PriceRule- Los métodos con cuerpo (
defaultystatic) no cuentan. Tampoco los métodos deObject:Comparatordeclaraboolean equals(Object obj), pero sigue siendo funcional. - Una lambda no es una clase anónima. Dentro de ella,
thises el objeto en el que está escrita, no la propia lambda.
La idea
Las incorporadas: Predicate, Function, Consumer, Supplier
No hace falta escribir una interfaz para cada regla. java.util.function tiene interfaces listas para los casos más comunes:
Predicate<T>: recibe unT, devuelve unboolean. El método estest. Es una condición: «¿el pedido es grande?».Function<T, R>: recibe unT, devuelve unR. El método esapply. Convierte una cosa en otra: un pedido en una etiqueta.Consumer<T>: recibe unT, no devuelve nada. El método esaccept. Hace algo con él: lo imprime, lo guarda.Supplier<T>: no recibe nada, devuelve unT. El método esget. Te da un objeto nuevo cuando se lo pides.
Predicate<Order> big = o -> o.getTotal() >= 50;Function<Order, String> label = o -> o.getCustomer().toUpperCase();Consumer<String> print = text -> System.out.println("-> " + text);Supplier<Order> guest = () -> new Order("Invitado", 0);
Order order = new Order("María", 75.50);System.out.println(big.test(order));print.accept(label.apply(order));System.out.println(guest.get().getCustomer());true-> MARÍAInvitadoand, or y negate («no»). Las funciones se encadenan una tras otra con andThen.Más precisamente, para curiosos
- Hay versiones para dos parámetros:
BiFunction<T, U, R>,BiPredicate<T, U>,BiConsumer<T, U>.UnaryOperator<T>esFunction<T, T>, yBinaryOperator<T>esBiFunction<T, T, T>. - Para los tipos primitivos existen
IntPredicate,IntFunction<R>,ToDoubleFunction<T>y otras. Con ellas, unintno se envuelve en unIntegeren cada llamada. f.andThen(g)hace primerofy luegog.f.compose(g)hace lo contrario: primerogy luegof.
Pruébalo
Condiciones combinadas
¿Qué imprimirá el programa?
Predicate<Order> big = o -> o.getTotal() >= 50;Predicate<Order> maria = o -> o.getCustomer().equals("María");
Order small = new Order("María", 30);Order large = new Order("Jorge", 80);
System.out.println(big.and(maria).test(small));System.out.println(big.or(maria).test(small));System.out.println(big.negate().test(large));big.and(maria) es false, y big.or(maria) es true, porque es de María. El pedido de Jorge es de 80 €, es grande, y negate() invierte la respuesta: false.Míralo
Las listas y los mapas reciben lambdas
List<String> cart = new ArrayList<>(List.of("pan", "", "miel", " ", "azúcar"));cart.removeIf(item -> item.isBlank());cart.forEach(item -> System.out.println(item));
Map<String, List<String>> byCity = new TreeMap<>();byCity.computeIfAbsent("Granada", city -> new ArrayList<>()).add("María");byCity.computeIfAbsent("Madrid", city -> new ArrayList<>()).add("Jorge");byCity.computeIfAbsent("Granada", city -> new ArrayList<>()).add("Elena");byCity.forEach((city, names) -> System.out.println(city + ": " + names));panmielazúcarGranada: [María, Elena]Madrid: [Jorge]removeIf(condición)elimina todos los elementos para los que la condición es verdadera. Es unPredicate.forEach(acción)hace la acción con cada elemento. Es unConsumer.computeIfAbsent(clave, función)devuelve el valor de la clave. Si no lo hay, llama a la función, pone el resultado en el mapa y lo devuelve. Así agrupar por ciudad ocupa una línea, sinif (!byCity.containsKey(...)).- El
forEachdel mapa recibe una lambda con dos parámetros: la clave y el valor.
TreeMap mantiene las claves ordenadas, por eso Granada va antes que Madrid.
La idea
Referencias a métodos: ::
s -> System.out.println(s). Entonces puedes escribir solo qué método es:List<String> names = List.of("maría", "jorge");names.forEach(System.out::println);
Function<String, String> upper = String::toUpperCase;Function<String, Integer> parse = Integer::parseInt;Supplier<List<String>> newList = ArrayList::new;
System.out.println(upper.apply("elena"));System.out.println(parse.apply("42") + 1);List<String> list = newList.get();list.add("café");System.out.println(list);maríajorgeELENA43[café]Esto se llama referencia a método. Hay cuatro tipos:
- un método de un objeto concreto:
System.out::printlness -> System.out.println(s); - un método del parámetro:
String::toUpperCaseess -> s.toUpperCase(); - un método estático:
Integer::parseIntess -> Integer.parseInt(s); - un constructor:
ArrayList::newes() -> new ArrayList<>().
Si la lambda hace algo más, por ejemplo s -> s.trim().toUpperCase(), déjala como lambda.
Míralo
Ordenar por varios campos
Comparator.comparing recibe una función que extrae la clave por la que se compara, y con ella crea un comparador. Le añades la siguiente regla con thenComparing, y reversed() invierte la dirección.
Los estudiantes se ordenan por nota, de la más alta a la más baja, y por nombre cuando las notas son iguales:
List<Student> students = new ArrayList<>(List.of( new Student("Juan", 5.50), new Student("Ana", 6.00), new Student("Pedro", 5.50), new Student("Bruno", 4.75)));students.sort(Comparator.comparing(Student::getGrade).reversed() .thenComparing(Student::getName));Ana 6.0Juan 5.5Pedro 5.5Bruno 4.75Se lee de izquierda a derecha: «compara por nota, invierte, luego compara por nombre cuando son iguales». Ni un solo if, ni una sola clase anónima.
Más precisamente, para curiosos
- Con una lambda en lugar de una referencia, a veces
reversed()no compila:
orders.sort(Comparator.comparing(o -> o.getTotal()).reversed());Main.java:26: error: cannot find symbol orders.sort(Comparator.comparing(o -> o.getTotal()).reversed()); ^ symbol: method getTotal() location: variable o of type ObjectEl compilador deduce el tipo de o de dentro hacia fuera. Cuando llega a .reversed(), todavía no sabe que se trata de Order, así que supone Object. La solución es una referencia o un tipo en el parámetro:
orders.sort(Comparator.comparing(Order::getTotal).reversed());orders.sort(Comparator.comparing((Order o) -> o.getTotal()).reversed());comparingIntycomparingDoublehacen lo mismo sin envolver enIntegeryDouble.Comparator.naturalOrder()yComparator.reverseOrder()ordenan según el orden natural del tipo: los números por tamaño, el texto alfabéticamente.
Pruébalo
¿Qué invierte reversed()?
reversed() está al final, después de thenComparing.¿Qué imprimirá el programa?
List<Student> students = new ArrayList<>(List.of( new Student("Juan", 5.50), new Student("Ana", 6.00), new Student("Pedro", 5.50), new Student("Bruno", 4.75)));
students.sort(Comparator.comparing(Student::getGrade) .thenComparing(Student::getName) .reversed());students.forEach(s -> System.out.println(s.getName() + " " + s.getGrade()));reversed() invierte el comparador sobre el que se llama. Ese comparador ya es «por nota, luego por nombre», así que se invierten los dos: las notas van de la más alta a la más baja, y Pedro va antes que Juan. Si quieres invertir solo las notas, reversed() va justo después de comparing(Student::getGrade), como en la tarjeta anterior.Pruébalo
Encuentra el error
Elige la línea con el error.
La lambda cambia total, y eso no está permitido:
Main.java:9: error: local variables referenced from a lambda expression must be final or effectively final prices.forEach(p -> total += p); ^Una lambda puede ejecutarse más tarde y en otro lugar, así que recibe una copia del valor de la variable local. Si la variable pudiera cambiar, la copia y el original dejarían de coincidir. Por eso, en una lambda Java solo permite variables locales que no cambian después de su primer valor. Se llaman efectivamente final (effectively final): funcionan incluso sin la palabra final. Esto también vale para un cambio fuera de la lambda: si más abajo escribes total = 5, la lambda tampoco puede leer total.
Para una suma, usa un bucle normal. En la próxima lección también lo harás en una línea.
Más precisamente, para curiosos
Leer una variable así está permitido:
double rate = 0.2;PriceRule discount = price -> price * (1 - rate);System.out.println(discount.apply(100));80.0Los campos de un objeto no son variables locales, y una lambda puede cambiarlos. No es una forma de saltarse la regla: si las lambdas se ejecutan en varios hilos al mismo tiempo, un campo compartido es una fuente de errores. Los hilos están en el módulo «Programas en el mundo real».
Ejercicio
Reglas como valores
Predicate, Function y las demás, añade import java.util.function.*;.1. Ordenar por varios campos. Una clase Product con nombre, categoría y precio. Ordena los productos por categoría, dentro de una categoría del más caro al más barato, y por nombre cuando el precio es igual. Los productos son: ratón, accesorios, 25 €; portátil, computadoras, 1299 €; teclado, accesorios, 49,90 €; tableta, computadoras, 399 €; auriculares, accesorios, 49,90 €.
Accesorios | Auriculares | 49.90 €Accesorios | Teclado | 49.90 €Accesorios | Ratón | 25.00 €Computadoras | Portátil | 1299.00 €Computadoras | Tableta | 399.00 €2. Filtrar pedidos. Añade una ciudad a Order. Escribe un método select(List<Order> orders, Predicate<Order> rule) que devuelva una lista nueva solo con los pedidos para los que la regla es verdadera. Haz dos reglas, madrid y big (al menos 50 €), e imprime los pedidos con madrid.and(big), con madrid.negate() y con «Madrid o al menos 100 €». Los pedidos: María, Madrid, 75,50 €; Jorge, Valencia, 12,90 €; Elena, Madrid, 40 €; Diego, Granada, 120 €.
Madrid, desde 50 €: María, Madrid: 75.50 €Fuera de Madrid: Jorge, Valencia: 12.90 € Diego, Granada: 120.00 €Envío gratis (Madrid o desde 100 €): María, Madrid: 75.50 € Elena, Madrid: 40.00 € Diego, Granada: 120.00 €Function<Double, Double>: un descuento de estudiante (−10 %), un descuento de Black Friday (−20 €) y «no menos de cero». Encadénalos con andThen en dos órdenes: primero el de estudiante y primero el del viernes. Pruébalo con 120 € y con 15 €. ¿Importa el orden?120.00 € -> estudiante y luego viernes: 88.00 €; viernes y luego estudiante: 90.00 €15.00 € -> estudiante y luego viernes: 0.00 €; viernes y luego estudiante: 0.00 €Más tareas
- Limpiar una lista. De una lista de correos, elimina los vacíos y los que no tienen
@con un soloremoveIf, y luego imprime el resto conforEach(System.out::println). - Estudiantes por clase. Agrupa estudiantes por clase (
"10A","10B") en unTreeMap<String, List<String>>concomputeIfAbsente imprime el mapa conforEach. - Tu propia interfaz. Haz
@FunctionalInterface interface Validator { boolean check(String value); }y tres validadores como lambdas: texto no vacío, solo dígitos, longitud de hasta 10. Pruébalos con algunas cadenas.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces · Listas · HashMap y HashSet · Clases anidadas
- Continúa en: Streams · record, switch y patrones · Patrones de diseño · Tests con JUnit
- Ejercicios sobre el tema: Java moderno