1. Primeros pasos
En pocas palabras: una expresión lambda es una forma de crear rápidamente una implementación de una interfaz funcional «al vuelo», sin declarar una clase aparte ni una clase anónima. Es como un mini-método sin nombre que se puede pasar como argumento o guardar en una variable.
Durante mucho tiempo, Java fue un lenguaje POO «clásico». Si querías pasar un trozo de comportamiento, por ejemplo, qué hacer al pulsar un botón, había que escribir clases anónimas:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("¡Botón pulsado!");
}
});
Con Java 8 llegó la magia de las lambdas:
button.setOnClickListener(() -> System.out.println("¡Botón pulsado!"));
Donde () -> System.out.println("¡Botón pulsado!") es precisamente una expresión lambda.
Formalmente: una expresión lambda es una forma compacta de escribir la implementación del único método abstracto de una interfaz funcional.
¿Por qué es importante?
- Permitir pasar comportamiento como un valor (por ejemplo, qué hacer con cada elemento de una lista).
- Escribir código compacto y legible.
- Usar las API modernas de Java: Stream API, manejo de eventos, tareas asíncronas y mucho más.
2. Sintaxis de las expresiones lambda
Forma general
(parámetros) -> expresión
// o
(parámetros) -> { bloque de código }
Ejemplos para distintas interfaces
1. Sin parámetros (por ejemplo, Runnable):
Runnable r = () -> System.out.println("¡Hola, lambda!");
r.run();
2. Un parámetro (por ejemplo, Consumer):
Consumer<String> printer = s -> System.out.println("Has pasado: " + s);
printer.accept("Java");
Si el parámetro es único y su tipo se puede inferir, se pueden omitir los paréntesis.
El tipo String es el tipo de la variable (s) que pasamos allí.
3. Varios parámetros (por ejemplo, Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
El tipo Integer es el tipo de las variables (a, b) que pasamos.
4. Cuerpo multilínea (se necesitan llaves y return si hay retorno explícito):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
El primer Integer es el tipo del resultado de la función; el segundo Integer es el tipo del parámetro.
Atajos y concisión
- Si el cuerpo es una sola expresión, se pueden omitir las llaves y return.
- Si no hay parámetros, se escriben paréntesis vacíos: () -> ...
- Si hay un solo parámetro, se pueden omitir los paréntesis: x -> ...
- Si hay más de un parámetro, se necesitan paréntesis: (a, b) -> ...
Tabla resumen
Veamos cómo se expresa la misma idea con una lambda y con una clase anónima:
| Interfaz | Expresión lambda (ejemplo) | Equivalente con clase anónima |
|---|---|---|
|
|
|
|
|
|
|
|
|
Ejemplo con una interfaz propia
Supongamos que tenemos una interfaz funcional:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
Antes se implementaba así:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
Ahora — a la moderna:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
Ejemplos para interfaces estándar
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("manzana", "plátano", "kiwi");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. Ámbito y captura de variables
Variables del contexto externo (effectively final)
Las expresiones lambda pueden utilizar variables del método circundante. Pero hay una regla: dichas variables deben ser effectively final, es decir, o bien declaradas explícitamente como final, o bien no modificarse después de su inicialización.
Ejemplo:
String prefix = "Resultado: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix aquí está “congelado”: después de esto no se puede cambiar
System.out.println(f.apply(5)); // Resultado: 10
Si intentas modificar prefix después de que se haya usado en la lambda, el compilador dará un error.
¿Por qué?
Una expresión lambda puede invocarse después de salir del método en el que se declaró la variable. Para evitar errores «mágicos», Java permite utilizar solo variables inmutables.
Diferencia con las clases anónimas
En las clases anónimas rige la misma regla: las variables del método externo deben ser final/effectively final. Pero hay matices con el ámbito: dentro de una clase anónima, this hace referencia a la propia clase anónima, mientras que en una lambda hace referencia a la clase externa.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // Imprimirá: Demo$1 (clase anónima)
}
};
Runnable r2 = () -> System.out.println(this); // Imprimirá: Demo (clase externa)
r1.run();
r2.run();
}
}
Lambdas y campos de clase
Una expresión lambda puede acceder a los campos de la clase externa sin restricciones:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
Aquí base se puede modificar (es un campo de clase).
x debe ser effectively final.
4. Práctica: escribamos algunas expresiones lambda
Ejemplo: filtrado de una lista de números
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("Par: " + n));
Aprenderás más sobre la Stream API en el nivel 30 :P
Ejemplo: función de transformación de cadena
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
Ejemplo: interfaz funcional propia
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Hola")); // ¡Hola!
Ejemplo: uso de variables del contexto externo
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// después de esto no se puede modificar factor
5. Errores típicos al trabajar con expresiones lambda
Error n.º 1: intentar modificar una variable capturada por la lambda.
Este código no compilará — la variable debe ser effectively final:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Error: ¡sum no es final!
Si necesitas acumular valores, usa un array o un objeto envoltorio.
Error n.º 2: confusión con el ámbito de this.
Dentro de una expresión lambda, this se refiere a la clase externa, no a la lambda (a diferencia de una clase anónima).
Error n.º 3: olvidar las llaves y return en una lambda multilínea.
Si el cuerpo de la lambda no es una sola expresión, se necesitan llaves y return:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
Error n.º 4: determinar incorrectamente el tipo de la expresión lambda.
Una expresión lambda siempre implementa una interfaz funcional. No puedes simplemente escribir:
var f = x -> x + 1; // ¡Error! Se desconoce qué tipo de interfaz es.
Hay que indicar el tipo explícitamente:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION