Ir al contenido
wannacode

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.

Order
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());
}
Salida
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 entonces return es 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());
}
Salida
Jorge: 12.90 €
Elena: 40.00 €
María: 75.50 €
Una lambda es un valor: puedes guardarla en una variable y pasarla más tarde.

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));
Salida
40.0
45.0
student 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
  1. La lambda es un objeto de tipo PriceRule, y student apunta a él. Su cuerpo todavía no se ejecuta.
  2. Una segunda regla, un segundo objeto. No se escribe ni una sola clase.
  3. student.apply(50) pasa 50 a la lambda: price es 50, y price * 0.8 da 40.0.
  4. minusFive.apply(50): el mismo número, otra regla, 45.0.
Más precisamente, para curiosos
  • La anotación @FunctionalInterface encima de la interfaz hace que el compilador compruebe que hay exactamente un método. Si añades un segundo método abstracto:
@FunctionalInterface
interface PriceRule {
double apply(double price);
String name();
}
Error
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 (default y static) no cuentan. Tampoco los métodos de Object: Comparator declara boolean equals(Object obj), pero sigue siendo funcional.
  • Una lambda no es una clase anónima. Dentro de ella, this es 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 un T, devuelve un boolean. El método es test. Es una condición: «¿el pedido es grande?».
  • Function<T, R>: recibe un T, devuelve un R. El método es apply. Convierte una cosa en otra: un pedido en una etiqueta.
  • Consumer<T>: recibe un T, no devuelve nada. El método es accept. Hace algo con él: lo imprime, lo guarda.
  • Supplier<T>: no recibe nada, devuelve un T. El método es get. 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());
Salida
true
-> MARÍA
Invitado
Las interfaces incorporadas también tienen métodos que combinan reglas. Los predicados se unen con and, 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> es Function<T, T>, y BinaryOperator<T> es BiFunction<T, T, T>.
  • Para los tipos primitivos existen IntPredicate, IntFunction<R>, ToDoubleFunction<T> y otras. Con ellas, un int no se envuelve en un Integer en cada llamada.
  • f.andThen(g) hace primero f y luego g. f.compose(g) hace lo contrario: primero g y luego f.

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));
Respuesta

Míralo

Las listas y los mapas reciben lambdas

Las colecciones tienen métodos que reciben una regla en lugar de un bucle:
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));
Salida
pan
miel
azúcar
Granada: [María, Elena]
Madrid: [Jorge]
  • removeIf(condición) elimina todos los elementos para los que la condición es verdadera. Es un Predicate.
  • forEach(acción) hace la acción con cada elemento. Es un Consumer.
  • 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, sin if (!byCity.containsKey(...)).
  • El forEach del 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: ::

A menudo una lambda solo pasa su parámetro a un método que ya existe: 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);
Salida
maría
jorge
ELENA
43
[café]

Esto se llama referencia a método. Hay cuatro tipos:

  • un método de un objeto concreto: System.out::println es s -> System.out.println(s);
  • un método del parámetro: String::toUpperCase es s -> s.toUpperCase();
  • un método estático: Integer::parseInt es s -> Integer.parseInt(s);
  • un constructor: ArrayList::new es () -> 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));
Salida
Ana 6.0
Juan 5.5
Pedro 5.5
Bruno 4.75

Se 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());
Error
Main.java:26: error: cannot find symbol
orders.sort(Comparator.comparing(o -> o.getTotal()).reversed());
^
symbol: method getTotal()
location: variable o of type Object

El 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());
  • comparingInt y comparingDouble hacen lo mismo sin envolver en Integer y Double.
  • Comparator.naturalOrder() y Comparator.reverseOrder() ordenan según el orden natural del tipo: los números por tamaño, el texto alfabéticamente.

Pruébalo

¿Qué invierte reversed()?

Aquí 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()));
Respuesta

Pruébalo

Encuentra el error

El programa debería imprimir la suma de los precios. No compila. ¿En qué línea está el error?

Elige la línea con el error.

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));
Salida
80.0

Los 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

Resuelve las tareas en tu editor. Para 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 €.

Salida
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 €.

Salida
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 €
3. Descuentos uno tras otro. Haz tres descuentos como 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?
Salida
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 solo removeIf, y luego imprime el resto con forEach(System.out::println).
  • Estudiantes por clase. Agrupa estudiantes por clase ("10A", "10B") en un TreeMap<String, List<String>> con computeIfAbsent e imprime el mapa con forEach.
  • 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

1. ¿Dónde puede ir una expresión lambda?
2. ¿Qué interfaz incorporada recibe un pedido y devuelve true o false?
3. ¿Qué puede reemplazar a la lambda s -> s.toUpperCase()?
4. Comparator.comparing(Student::getGrade).thenComparing(Student::getName).reversed(). ¿Qué invierte reversed()?
5. ¿Por qué int total = 0; list.forEach(x -> total += x); no compila?

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