Expresiones Lambda en Java
Implementaciones concisas en línea de interfaces funcionales en Java con expresiones lambda: (params) -> cuerpo.
Una expresión lambda es la sintaxis concisa que Java 8 añadió para "una instancia de una interfaz que tiene exactamente un método abstracto". Antes de ella, se escribía como una clase anónima. Después, se escribe como una lista de parámetros, una flecha y un cuerpo:
Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();No hay ningún tipo de valor nuevo aquí — r, byLen y length siguen siendo referencias a objetos, y en tiempo de ejecución cada uno contiene una instancia de una clase que implementa la interfaz de la izquierda. Lo que es nuevo es que el código que dice "hazme uno" es lo suficientemente corto para caber en el lugar de la llamada, lo que desbloquea todos los demás modismos funcionales de la parte: predicados de filtro, constructores de comparadores, manejadores de eventos, pipelines de stream.
Las formas de la sintaxis
Una lambda tiene tres partes: lista de parámetros, flecha -> y cuerpo. Cada parte tiene su forma abreviada:
// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");
// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length(); // same thing
// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();
// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;
// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;
// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
if (n == 0) return "zero";
if (n < 0) return "negative";
return "positive";
};Tres reglas los unen:
- Los tipos de parámetro generalmente se infieren del tipo destino (la interfaz declarada en el lugar de la llamada). Escríbelos solo cuando el compilador no pueda elegir uno o cuando mejoren la legibilidad.
- El cuerpo de expresión devuelve su valor implícitamente. Sin
return, sin punto y coma. La expresión es el resultado. - El cuerpo de bloque necesita
returncuando el método de la interfaz tiene un tipo de retorno. Olvidarlo es un error de compilación, no unnullsilencioso.
Tipado por destino — dónde pueden aparecer las lambdas
Una lambda no tiene un tipo intrínseco. El compilador determina su tipo a partir del destino — el contexto donde se usa:
Runnable r1 = () -> doWork(); // target: Runnable
Callable<Integer> c1 = () -> 42; // target: Callable<Integer>
Supplier<Integer> s1 = () -> 42; // target: Supplier<Integer>() -> 42 es la misma fuente en los tres casos, pero compila a tres instancias de interfaz distintas. Por eso una lambda no puede asignarse directamente a Object — Object o = () -> 42; es ambiguo y el compilador lo rechaza. Castea para desambiguar: Object o = (Supplier<Integer>) () -> 42;.
Los destinos más frecuentes son:
- Un parámetro de método tipado como interfaz funcional:
list.removeIf(s -> s.isEmpty()). - Un campo o variable local de tipo interfaz funcional:
Predicate<String> empty = String::isEmpty;. - Un tipo de retorno:
public Supplier<Date> now() { return Date::new; }.
Si no hay destino, no puede haber lambda. var f = s -> s.length(); no compila — var no puede inferir un tipo destino.
Captura de variables: "efectivamente final"
Una lambda puede leer variables locales del método que la contiene, pero solo si esas variables son efectivamente finales — nunca reasignadas después de su valor inicial:
int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier; // OK — `multiplier` never reassigned
multiplier = 4; // <-- this line would make the lambda not compileLa regla es la misma que las clases internas anónimas siempre han tenido, y la razón es la misma: una lambda puede sobrevivir al método en que fue definida (podrías almacenarla en un campo, o pasarla a otro hilo), y Java no tiene cierres que capturen la variable — captura el valor en el momento de la construcción. Permitir la reasignación crearía una ilusión confusa.
Los campos son una historia diferente. Una lambda puede leer y mutar campos de instancia y estáticos libremente:
class Counter {
private int n = 0;
Runnable inc = () -> n++; // legal — `n` is a field, not a local
}Esta es una fuente frecuente de errores en código de stream — una lambda que muta un campo compartido parece inocente pero tiene condiciones de carrera cuando el stream se paraleliza. Las lambdas puras son más seguras.
this, return y break dentro de una lambda
Una lambda no es un nuevo ámbito para this. Dentro de una lambda, this hace referencia a la instancia que la contiene — igual que el código circundante:
class Greeter {
String prefix = "Hello, ";
Function<String, String> greet = name -> this.prefix + name; // `this` is the Greeter
}Esta es una de las diferencias prácticas más grandes respecto a las clases anónimas, donde this hacía referencia a la propia instancia anónima.
return dentro de una lambda devuelve desde la lambda, no desde el método que la contiene. break y continue no funcionan en una lambda — pertenecen al bucle al que apuntan, y el cuerpo de la lambda no forma parte del bucle circundante.
Lambda vs clase anónima — cuándo usar cada una
Para interfaces funcionales, las lambdas son casi siempre más cortas y claras. Generan un bytecode ligeramente diferente (invokedynamic) y no crean un nuevo archivo de clase por lugar de uso, por lo que también son típicamente más ligeras en tiempo de ejecución.
Usa una clase anónima cuando:
- La interfaz tiene más de un método abstracto (no es funcional).
- Necesitas un campo local al método (
int seen = 0;accesible entre llamadas). - Necesitas que
thishaga referencia a la instancia que estás creando, no a la instancia que la contiene. - Necesitas sobreescribir un método predeterminado para especializar su comportamiento.
En todos los demás casos, la lambda gana.
Un ejemplo trabajado: captura, tipado por destino, los cuatro lugares de llamada
El programa a continuación demuestra los cuatro lugares más comunes donde aparece una lambda — forEach de colección, removeIf, ordenación y filtro de stream — junto con las reglas de captura y el tipado por destino.
Lo que se puede extraer de la ejecución:
() -> \"hi\"funcionó tanto comoCallable<String>comoSupplier<String>— la misma fuente, diferentes tipos destino, diferentes instancias de interfaz. Por eso una lambda no tiene tipo hasta que el contexto proporciona uno.times = n -> n * factorcapturófactorpor valor. El compilador lo aceptó porquefactornunca fue reasignado. Descomentarfactor = 11convertiría afactoren una variable no efectivamente final y rompería la compilación de la lambda.forEach,removeIfysortaceptan cada uno una interfaz funcional diferente (Consumer,Predicate,Comparator), y la forma de la lambda — número de parámetros, presencia de retorno — coincidió con el único método abstracto de cada interfaz. El compilador hace la correspondencia mediante tipado por destino.- La lambda
describecon cuerpo de bloque necesitó declaracionesreturnexplícitas porque su destino (Function<Integer, String>) tiene un tipo de retorno novoid. Las lambdas con cuerpo de expresión anteriores devolvieron su expresión implícitamente.
Qué sigue
Ya conoces la sintaxis y las reglas de captura. La siguiente pregunta es: ¿a qué interfaz, exactamente, compila una lambda? Java Functional Interfaces presenta la regla de método abstracto único (SAM), la anotación @FunctionalInterface y cómo escribir tu propia interfaz funcional para los casos que la biblioteca estándar no cubre.