used - lambda expressions java 8 example
Lambdas: las variables locales necesitan variables de instancia finales, no (8)
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
En un lambda, las variables locales deben ser finales, pero las variables de instancia no. ¿Porque?
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.<hidden reference to outer 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.
Dentro de las expresiones Lambda puede usar efectivamente variables finales del alcance 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 compilador hace el truco en tu nombre e inserta "this" (this.i) .
private Integer i = 0;
public void process(){
Consumer<Integer> c = (i)-> System.out.println(++this.i);
c.accept(i);
}
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 pila. 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 programación imperativa típicos (que, como explicamos en capítulos posteriores, previenen la fácil paralelización) que mutan una variable externa.
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 compilació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 compilador 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.
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 alcance 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 alcance variable ; imagine un constructor de copia agregado para todas las variables locales que pasa a un cierre.
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 compilació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.
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 pila :
Cada vez que se crea un objeto, siempre se almacena en el espacio Heap y la memoria de la pila contiene la referencia al mismo. La memoria de pila solo contiene variables primitivas locales y variables de referencia para objetos en el espacio de pila.
Para resumir, hay dos puntos que creo que realmente importan:
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);
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.