Historia de la cuestión:
Antes de Java 8, para utilizar variables del ámbito externo en una clase interna o clase anónima, estas variables debían declararse obligatoriamente como final. En Java 8, este requisito se flexibilizó: ahora una variable puede no estar declarada como final si de hecho no se cambia (effectively final).
Problema:
Las expresiones lambda y las clases internas usan cierres sobre las variables del bloque externo. Sin embargo, si estas variables cambian de valor, surge confusión y comportamiento incorrecto — no es posible entender qué valor usar.
Solución:
El compilador permite usar una variable en una clase interna o lambda solo si es effectively final — es decir, nunca se ha cambiado después de su inicialización, incluso si no está declarada explícitamente como final.
Ejemplo de código:
public void demo() { int x = 10; Runnable r = () -> System.out.println(x); // x — effectively final r.run(); }
Si intentamos cambiar x:
public void demo() { int x = 10; x = 20; // ahora x no es efectivamente final Runnable r = () -> System.out.println(x); // Error de compilación }
Características clave:
¿Se pueden usar objetos mutables si la referencia en sí es effectively final?
Sí, si la referencia no cambia, pero el objeto a la que apunta se modifica — esto es permitido. Por ejemplo,
List<String> list = new ArrayList<>(); list.add("A"); Runnable r = () -> System.out.println(list.get(0)); // OK list = new ArrayList<>(); // Si se hace esto, habrá un error de compilación
¿Se puede declarar una variable como final y luego aún así cambiar el contenido del objeto?
Sí. final se refiere a la referencia, no al contenido del objeto. Cambiar el estado del objeto a través de la referencia es permitido. Por ejemplo,
final List<Integer> nums = new ArrayList<>(); nums.add(5); // OK nums = new ArrayList<>(); // Error
¿Se pueden usar variables de parámetros del método en lambdas?
Sí, si también son effectively final — es decir, no se cambian dentro del método después de la inicialización.
En el método se utilizan variables que se sobrescriben accidentalmente después de crear la lambda, como resultado, el programa no compila y se pierde tiempo tratando de averiguar la causa.
Ventajas:
Desventajas:
El desarrollador utiliza un valor incrementable a través de AtomicInteger (o algún otro objeto contenedor), que mantiene la referencia, no el valor, asegurando el correcto funcionamiento de las lambdas, incluso si se requiere modificar el contador dentro de la expresión lambda.
Ventajas:
Desventajas: