1. Introducción a los closures
Un closure — es una función (o un objeto-función) que no solo utiliza sus propios parámetros, sino que también «recuerda» variables del contexto circundante donde fue creada. Dicho de forma sencilla, si una expresión lambda o una clase anónima dentro de un método usa variables de ese método — se convierte en un closure.
Ejemplo sencillo
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "¡Hola, ";
Runnable sayHello = () -> System.out.println(greeting + "mundo!");
sayHello.run(); // Imprimirá: ¡Hola, mundo!
}
}
Aquí la lambda ha «capturado» la variable greeting del método externo y la usa dentro de sí. ¡Esto es un closure!
2. Variables efectivamente final: ¿qué son y para qué?
En Java las expresiones lambda (y las clases anónimas) solo pueden usar aquellas variables del método externo que estén declaradas como final o que no cambien después de su inicialización. Dichas variables se llaman efectivamente final.
¿Por qué?
Esta restricción se debe a que una expresión lambda puede ejecutarse después de que termine el método en el que se creó. Si la variable pudiera cambiar, habría confusión: ¿qué versión de la variable usar? Para evitar sorpresas, Java exige que la variable sea inmutable (o al menos que parezca inmutable).
Ejemplo: uso correcto
public static void main(String[] args) {
int number = 42; // number — efectivamente final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Ejemplo: intento de cambiar la variable tras usarla
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // ERROR: la variable number debe ser final o efectivamente final
r.run();
}
El compilador mostrará un error: Variable used in lambda expression should be final or effectively final.
Efectivamente final — es... una variable a la que se asigna exactamente una vez y no cambia después. No es obligatorio escribir final, el compilador lo deducirá.
3. ¿Cómo capturan variables las expresiones lambda?
Cuando escribes una lambda que usa una variable externa, Java «empaqueta» esa variable junto con la lambda. Incluso si el método donde se creó la lambda ya terminó, la variable no desaparece: vive dentro del closure.
Ilustración: la lambda «recuerda» la variable
public static Runnable createGreeter(String name) {
// name — parámetro del método, será capturado por la lambda
return () -> System.out.println("¡Hola, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("John");
greeter.run(); // ¡Hola, John!
}
Aquí la variable name ya no existe en la pila del método main, pero greeter sigue «recordando» su valor.
¿Cómo está implementado por dentro?
El compilador de Java crea un objeto auxiliar especial (también llamado «capture/display class») que almacena todas las variables capturadas. La expresión lambda se convierte en un objeto que tiene una referencia a este «contenedor» con las variables.
4. Ejemplo de closure: devolvemos una función que usa una variable
Escribamos una función que devuelve una lambda que usa una variable de su contexto:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x — es capturada por la lambda
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Aquí la variable x ya «salió» de la pila del método, pero la lambda aún puede usarla.
5. ¿Por qué no se puede modificar una variable capturada?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // ¡ERROR!
printer.run();
}
El compilador no lo permitirá. Si pudiéramos cambiar base, no estaría claro qué versión de la variable usar en la lambda: ¿la antigua o la nueva? Por eso Java prohíbe modificar variables locales capturadas por una lambda.
¿Qué se puede utilizar en una lambda?
- Variables locales que no cambian tras su inicialización (efectivamente final).
- Campos de clase (tanto static como no estáticos): se pueden cambiar, pero es otro mecanismo (acceso al estado del objeto), no una captura de variable local.
6. Comparación con las clases anónimas
Antes de que existieran las expresiones lambda en Java, se podían crear closures mediante clases anónimas:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
Las reglas son las mismas: la variable word debe ser final o efectivamente final.
Diferencia: el ámbito de this
- En una clase anónima, this se refiere a la instancia de la clase anónima.
- En una expresión lambda, this se refiere al objeto externo (por ejemplo, la instancia actual de la clase).
7. Closures y campos de clase
Si una lambda usa un campo de clase, esto no es «captura de variable local» en sentido estricto: el campo siempre está disponible y puede cambiarse.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Contador: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Contador: 1
r.run(); // Contador: 2
}
}
8. Errores típicos y particularidades de los closures en Java
Error n.º 1: intentar modificar una variable capturada por una lambda. El error más común es intentar cambiar una variable local después de que se haya usado en una lambda. El compilador indicará: Variable used in lambda expression should be final or effectively final.
Error n.º 2: esperar que la variable esté «congelada». En Java, una variable capturada no es una copia del valor, sino una referencia al original cuando se trata de un campo de clase. Si el campo de clase cambia, la lambda verá el nuevo valor. Pero las variables locales en una lambda deben ser únicamente efectivamente final.
Error n.º 3: pensar que la lambda crea un nuevo ámbito para this. En una lambda, this es el objeto externo (la clase contenedora). En una clase anónima, this es la propia clase anónima.
Error n.º 4: uso de objetos mutables. Si capturas una referencia a un objeto mutable (por ejemplo, una lista), puedes modificar su contenido dentro de la lambda, incluso si la variable es efectivamente final:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
Aquí la variable list no cambia (no hacemos list = ...), pero el objeto al que apunta sí cambia.
GO TO FULL VERSION