Lambdas: las variables locales necesitan variables de instancia finales, no

En un lambda, las variables locales deben ser finales, pero las variables de instancia no. ¿Porque?

La diferencia fundamental entre un campo y una variable local es que la variable local se copia cuando JVM crea una instancia lambda. Por otro lado, los campos se pueden cambiar libremente, porque los cambios a ellos también se propagan a la instancia de clase externa (su scope es toda la clase exterior, como señaló Boris a continuación).

La manera más fácil de pensar sobre clases anónimas, cierres y labmdas es desde la perspectiva del scope variable ; imagine un constructor de copia agregado para todas las variables locales que pasa a un cierre.

En el documento del proyecto lambda: Estado de la Lambda v4

En la Sección 7. Captura de variables , se menciona que …

Es nuestra intención prohibir la captura de variables locales mutables. La razón es que modismos como este:

int sum = 0; list.forEach(e -> { sum += e.size(); }); 

son fundamentalmente seriales; es bastante difícil escribir cuerpos lambda como este que no tienen condiciones de carrera. A menos que estemos dispuestos a hacer cumplir, preferiblemente en tiempo de comstackción, que dicha función no puede escapar a su hilo de captura, esta característica bien puede causar más problemas de los que resuelve.

Editar:

Otra cosa a tener en cuenta aquí es que las variables locales se pasan en el constructor de la clase interna cuando se accede a ellas dentro de su clase interna, y esto no funcionará con la variable no final porque el valor de las variables no finales se puede cambiar después de la construcción.

Mientras que en caso de variable de instancia, el comstackdor pasa referencia de clase y la referencia de clase se usará para acceder a la variable de instancia. Por lo tanto, no es necesario en el caso de variables de instancia.

PD: Vale la pena mencionar que las clases anónimas solo pueden acceder a las variables locales finales (en JAVA SE 7), mientras que en Java SE 8 se puede acceder efectivamente a las variables finales también dentro de lambda así como a las clases internas.

Debido a que las variables de instancia siempre se accede a través de una operación de acceso de campo en una referencia a algún objeto, es decir, some_expression.instance_variable . Incluso cuando no accede explícitamente a través de la notación de puntos, como instance_variable , se trata implícitamente como this.instance_variable (o si se encuentra en una clase interna que accede a la variable de instancia de una clase externa, OuterClass.this.instance_variable , que está bajo el capó this..instance_variable ).

Por lo tanto, nunca se accede directamente a una variable de instancia, y la “variable” real a la que se accede directamente es this (que es “efectivamente final” ya que no es asignable), o una variable al comienzo de alguna otra expresión.

Parece que estás preguntando sobre variables a las que puedes hacer referencia desde un cuerpo lambda.

Del JLS §15.27.2

Cualquier variable local, parámetro formal o parámetro de excepción utilizado pero no declarado en una expresión lambda debe declararse final o efectivamente final (§4.12.4), o se produce un error en tiempo de comstackción cuando se intenta utilizar.

Por lo tanto, no necesita declarar las variables como final , solo necesita asegurarse de que sean “efectivamente definitivas”. Esta es la misma regla que se aplica a las clases anónimas.

En Java 8 en Action book, esta situación se explica como:

Es posible que se pregunte por qué las variables locales tienen estas restricciones. En primer lugar, existe una diferencia clave en cómo las variables locales y de instancia se implementan detrás de las escenas. Las variables de instancia se almacenan en el montón, mientras que las variables locales viven en la stack. Si un lambda podía acceder a la variable local directamente y el lambda se utilizaba en un hilo, entonces el hilo que utilizaba el lambda podía intentar acceder a la variable una vez que el hilo que asignaba la variable lo había desasignado. Por lo tanto, Java implementa el acceso a una variable local gratuita como el acceso a una copia en lugar de acceder a la variable original. Esto no hace ninguna diferencia si la variable local se asigna solo una vez, de ahí la restricción. En segundo lugar, esta restricción también desalienta los patrones de progtwigción imperativa típicos (que, como explicamos en capítulos posteriores, previenen la fácil paralelización) que mutan una variable externa.

Dentro de las expresiones Lambda puede usar efectivamente variables finales del scope circundante. Efectivamente significa que no es obligatorio declarar la variable final, pero asegúrese de no cambiar su estado dentro de la expresión lambda.

También puede usar esto dentro de los cierres y usar “this” significa el objeto que lo incluye pero no el lambda en sí, ya que los cierres son funciones anónimas y no tienen clases asociadas.

Entonces, cuando usas cualquier campo (digamos intérprete privado i;) de la clase adjunta que no se declara final y no es efectivamente final, seguirá funcionando ya que el comstackdor hace el truco en tu nombre e inserta “this” (this.i) .

 private Integer i = 0; public void process(){ Consumer c = (i)-> System.out.println(++this.i); c.accept(i); } 

Aquí hay un ejemplo de código, ya que tampoco esperaba esto, esperaba no poder modificar nada fuera de mi lambda

  public class LambdaNonFinalExample { static boolean odd = false; public static void main(String[] args) throws Exception { //boolean odd = false; - If declared inside the method then I get the expected "Effectively Final" compile error runLambda(() -> odd = true); System.out.println("Odd=" + odd); } public static void runLambda(Callable c) throws Exception { c.call(); } } 

Salida: impar = verdadero

SÍ, puede cambiar las variables miembro de la instancia, pero NO PUEDE cambiar la instancia en sí, al igual que cuando maneja las variables .

Algo como esto como se menciona:

  class Car { public String name; } public void testLocal() { int theLocal = 6; Car bmw = new Car(); bmw.name = "BMW"; Stream.iterate(0, i -> i + 2).limit(2) .forEach(i -> { // bmw = new Car(); // LINE - 1; bmw.name = "BMW NEW"; // LINE - 2; System.out.println("Testing local variables: " + (theLocal + i)); }); // have to comment this to ensure it's `effectively final`; // theLocal = 2; } 

El principio básico para restringir las variables locales es sobre los datos y la validez de cálculo

Si el lambda, evaluado por el segundo subproceso, se le dio la capacidad de mutar las variables locales. Incluso la capacidad de leer el valor de variables locales mutables de un hilo diferente introduciría la necesidad de sincronización o el uso de volátiles para evitar leer datos obsoletos.

Pero como sabemos el propósito principal de las lambdas

Entre las diferentes razones para esto, la más apremiante para la plataforma Java es que hacen que sea más fácil distribuir el procesamiento de colecciones a través de múltiples hilos .

A diferencia de las variables locales, la instancia local puede ser mutada, porque se comparte globalmente. Podemos entender esto mejor a través de la diferencia de montón y stack :

Cada vez que se crea un objeto, siempre se almacena en el espacio Heap y la memoria de la stack contiene la referencia al mismo. La memoria de stack solo contiene variables primitivas locales y variables de referencia para objetos en el espacio de stack.

Para resumir, hay dos puntos que creo que realmente importan:

  1. Es realmente difícil hacer que la instancia sea efectivamente definitiva , lo que puede causar una gran cantidad de carga insensata (solo imagine la clase anidada en profundidad);

  2. la instancia en sí misma ya está compartida a nivel mundial y lambda también se puede compartir entre los hilos, por lo que pueden funcionar juntos de manera adecuada ya que sabemos que estamos manejando la mutación y queremos pasar esta mutación;

El punto de equilibrio aquí es claro: si sabes lo que estás haciendo, puedes hacerlo fácilmente, pero si no, la restricción predeterminada te ayudará a evitar errores insidiosos .

PD Si la sincronización requerida en la mutación de instancia , puede usar directamente los métodos de reducción de flujo o si hay un problema de dependencia en la mutación de la instancia , aún puede usar thenApply o thenCompose in Function al mapping o métodos similares.

Poniendo algunos conceptos para futuros visitantes:

Básicamente, todo se reduce al punto de que el comstackdor debería poder determinar de manera determinista que el cuerpo de la expresión lambda no está trabajando en una copia obsoleta de las variables .

En el caso de variables locales, el comstackdor no tiene forma de asegurarse de que el cuerpo de expresión lambda no está trabajando en una copia obsoleta de la variable a menos que esa variable sea final o efectivamente final, por lo que las variables locales deben ser finales o efectivamente finales.

Ahora, en el caso de campos de instancia, cuando accede a un campo de instancia dentro de la expresión lambda, el comstackdor agregará this a ese acceso variable (si no lo ha hecho explícitamente) y dado que this es efectivamente final, el comstackdor está seguro de que la expresión lambda body siempre tendrá la última copia de la variable (tenga en cuenta que el multi-threading está fuera del scope en este momento para esta discusión). Entonces, en los campos de instancia de caso, el comstackdor puede decir que el cuerpo lambda tiene la última copia de la variable de instancia, por lo que las variables de instancia no tienen que ser definitivas o efectivamente definitivas. Consulte la siguiente captura de pantalla desde una diapositiva de Oracle:

enter image description here

Además, tenga en cuenta que si está accediendo a un campo de instancia en la expresión lambda y que se está ejecutando en un entorno de subprocesos múltiples, es posible que se ejecute en un problema.