Comportamiento que puedes pasar de un lado a otro
Una expresión lambda es un bloque de código compacto que puedes entregar a un método, almacenar en una variable o devolver, igual que harías con un número o una cadena. Antes de las lambdas, pasar comportamiento significaba escribir toda una clase (o una verbosa clase anónima) solo para envolver un método. Una lambda lo reduce a su esencia: los parámetros y el cuerpo.
Una lambda siempre implementa una interfaz funcional, es decir, una interfaz con exactamente un método abstracto (las conociste al final de la página sobre interfaces). El compilador deduce a qué interfaz te refieres a partir del contexto y asocia tu lambda con ese único método.
x -> x * 2 es la implementación completa de apply. Sin new, sin cuerpo de clase, sin nombre de método: la interfaz aporta todo eso.
La sintaxis de la flecha
Toda lambda tiene la forma parámetros -> cuerpo. Las piezas se ajustan según cuánto necesites:
() -> 42 // sin parámetros
x -> x + 1 // un parámetro, paréntesis opcionales
(x, y) -> x + y // dos o más parámetros necesitan paréntesis
(int x, int y) -> x + y // los tipos son opcionales: normalmente se infieren
x -> { // un cuerpo de bloque necesita llaves y return
int doubled = x * 2;
return doubled + 1;
}
Un cuerpo de una sola expresión (x -> x + 1) devuelve su valor de forma implícita, sin la palabra clave return. En cuanto usas llaves, estás escribiendo un bloque normal y debes usar return explícitamente si el método devuelve algo. Un error común es mezclar ambos: x -> { x + 1 } no compila, porque un bloque necesita una sentencia (return x + 1;).
Las lambdas reemplazan a las clases anónimas
La forma más clara de ver qué te aporta una lambda es el antes/después. Ordenar con un Comparator personalizado solía verse así:
// antes - clase anónima
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Lo mismo como lambda es una sola línea legible:
Comparator es una interfaz funcional (su único método abstracto es compare), así que la lambda encaja directamente. Toda la ceremonia (el new, la clase, la firma del método) desaparece, dejando solo la lógica de comparación.
La caja de herramientas de java.util.function
Rara vez necesitas declarar tu propia interfaz funcional. El paquete java.util.function incluye las formas más comunes, y casi todas las API de la biblioteca de Java las aceptan:
Function<T, R>- recibe unT, devuelve unR(apply)Predicate<T>- recibe unT, devuelve unboolean(test)Supplier<T>- no recibe nada, devuelve unT(get)Consumer<T>- recibe unT, no devuelve nada (accept)
Estas son interfaces genéricas: Function<String, Integer> reutiliza los genéricos que viste en la página anterior para mantener la seguridad de tipos. Elige la que tenga la forma que coincida con lo que tu código necesita recibir y producir.
Referencias a métodos
Cuando una lambda no hace más que llamar a un método ya existente, puedes reemplazarla por una referencia a método usando ::. Es el mismo valor, escrito de forma más directa:
Las referencias a métodos vienen en varios sabores: String::toUpperCase (un método de instancia llamado sobre cada argumento), Math::max (un método estático), System.out::println (un método sobre un objeto concreto) y ArrayList::new (un constructor). Recurre a una solo cuando se lea con claridad: si tienes que pensar qué forma aplica, una lambda normal está bien.
Captura de variables
Una lambda puede usar variables locales del método que la rodea, pero solo si son final o efectivamente final: asignadas una vez y nunca modificadas. Java captura el valor en el momento en que se crea la lambda, así que una variable que pudiera cambiar después sería ambigua.
Si reasignas factor en cualquier sitio, el compilador rechaza la lambda con "variable used in lambda expression should be final or effectively final". Cuando realmente necesites un estado mutable compartido, captura un objeto en su lugar (un campo, un elemento de un array o un AtomicInteger), porque la referencia permanece fija aunque su contenido cambie. Ten en cuenta que las lambdas, a diferencia de las clases anónimas, no crean su propio ámbito: this dentro de una lambda hace referencia a la instancia que la envuelve, no a la lambda en sí.
Siguiente: Streams
Las lambdas son el bloque de construcción, no el destino. Su verdadero beneficio llega con la API de Streams, donde encadenas operaciones como filter, map y reduce (cada una recibe una lambda) para expresar transformaciones de datos como una tubería legible en lugar de un enredo de bucles. Eso es lo que veremos en la siguiente página.
Preguntas frecuentes
¿Qué es una expresión lambda en Java?
Una lambda es una forma breve de escribir una instancia de una interfaz funcional, es decir, una interfaz con un único método abstracto. En lugar de toda una clase anónima, escribes parámetros -> cuerpo. El compilador asocia la lambda con el único método de la interfaz, por lo que Runnable r = () -> System.out.println("hi"); es un Runnable completo. Las lambdas te permiten pasar comportamiento de un lado a otro como si fueran datos.
¿Cuál es la diferencia entre una lambda y una referencia a método en Java?
Ambas producen una instancia de una interfaz funcional. Una lambda tiene parámetros explícitos y un cuerpo (s -> s.toUpperCase()), mientras que una referencia a método es una forma abreviada de una lambda que se limita a llamar a un método ya existente (String::toUpperCase). Usa una referencia a método cuando la lambda no haría más que reenviar sus argumentos a un único método con nombre: es más corta y se lee mejor.
¿Por qué las variables usadas en una lambda de Java deben ser final o efectivamente final?
Una lambda puede capturar variables locales del método que la envuelve, pero solo si nunca cambian tras la asignación; eso es lo que significa "efectivamente final". Java captura el valor, no una referencia viva a la variable, así que permitir la reasignación sería ambiguo e inseguro entre hilos. Si necesitas un estado mutable compartido, usa en su lugar un campo o un envoltorio como un array o AtomicInteger.