variable tutorial parameter method lambdas java8 expressions example java constructor lambda compiler-construction java-8

tutorial - lambda stream java



Java "Es posible que no se haya inicializado el campo final en blanco" Interfaz anónima vs Expresión Lambda (3)

No puedo reproducir el error para su caso final con el compilador de Eclipse.

Sin embargo, el razonamiento para el compilador de Oracle que puedo imaginar es el siguiente: dentro de una lambda, el valor de obj debe capturarse en el momento de la declaración. Es decir, se debe inicializar cuando se declara dentro del cuerpo lambda.

Pero, en este caso, Java debería capturar el valor de la instancia de Foo lugar de obj . Luego puede acceder a obj través de la referencia de objeto Foo (inicializada) e invocar su método. Así es como el compilador de Eclipse compila su pieza de código.

Esto se indica en la especificación, here :

La sincronización de la evaluación de la expresión de referencia del método es más compleja que la de las expresiones lambda (§15.27.4). Cuando una expresión de referencia de método tiene una expresión (en lugar de un tipo) que precede al :: separador, esa subexpresión se evalúa inmediatamente. El resultado de la evaluación se almacena hasta que se invoca el método del tipo de interfaz funcional correspondiente ; en ese punto, el resultado se utiliza como referencia de destino para la invocación. Esto significa que la expresión que precede al :: separador se evalúa solo cuando el programa encuentra la expresión de referencia del método y no se vuelve a evaluar en las invocaciones posteriores en el tipo de interfaz funcional.

Algo similar ocurre para

Object obj = new Object(); // imagine some local variable Runnable run = () -> { obj.toString(); };

Imagine obj es una variable local, cuando se ejecuta el código de expresión lambda, se evalúa obj y produce una referencia. Esta referencia se almacena en un campo en la instancia de Runnable creada. Cuando se llama a run.run() , la instancia usa el valor de referencia almacenado.

Esto no puede suceder si el objeto no está inicializado. Por ejemplo

Object obj; // imagine some local variable Runnable run = () -> { obj.toString(); // error };

La lambda no puede capturar el valor de obj , porque todavía no tiene un valor. Es efectivamente equivalente a

final Object anonymous = obj; // won''t work if obj isn''t initialized Runnable run = new AnonymousRunnable(anonymous); ... class AnonymousRunnable implements Runnable { public AnonymousRunnable(Object val) { this.someHiddenRef = val; } private final Object someHiddenRef; public void run() { someHiddenRef.toString(); } }

Así es como el compilador de Oracle se está comportando actualmente para su fragmento.

Sin embargo, el compilador de Eclipse, en cambio, no está capturando el valor de obj , está capturando el valor de this (la instancia de Foo ). Es efectivamente equivalente a

final Foo anonymous = Foo.this; // you''re in the Foo constructor so this is valid reference to a Foo instance Runnable run = new AnonymousRunnable(anonymous); ... class AnonymousRunnable implements Runnable { public AnonymousRunnable(Foo foo) { this.someHiddenRef = foo; } private final Foo someHiddenFoo; public void run() { someHiddenFoo.obj.toString(); } }

Lo cual está bien porque asume que la instancia de Foo está completamente inicializada cuando se invoca la run .

Recientemente he estado encontrando el mensaje de error "Puede que el campo final en blanco no se haya inicializado".

Por lo general, este es el caso si intenta referirse a un campo que posiblemente todavía no esté asignado a un valor. Ejemplo de clase:

public class Foo { private final Object obj; public Foo() { obj.toString(); // error (1) obj = new Object(); obj.toString(); // just fine (2) } }

Yo uso Eclipse. En la línea (1) me sale el error, en la línea (2) todo funciona. Hasta ahora eso tiene sentido.

A continuación, trato de acceder a obj dentro de una interfaz anónima que creo dentro del constructor.

public class Foo { private Object obj; public Foo() { Runnable run = new Runnable() { public void run() { obj.toString(); // works fine } }; obj = new Object(); obj.toString(); // works too } }

Esto también funciona, ya que no obj a obj en el momento en que creo la interfaz. También podría pasar mi instancia a otro lugar, luego inicializar el objeto obj y luego ejecutar mi interfaz. (Sin embargo, sería apropiado verificar si el null antes de usarlo). Todavía tiene sentido.

Pero ahora Runnable la creación de mi instancia de Runnable a la versión de la flecha de la hamburguesa usando una expresión lambda :

public class Foo { private final Object obj; public Foo() { Runnable run = () -> { obj.toString(); // error }; obj = new Object(); obj.toString(); // works again } }

Y aquí es donde ya no puedo seguir. Aquí recibo la advertencia de nuevo. Soy consciente de que el compilador no maneja las expresiones lambda como inicializaciones habituales, no lo "reemplaza por la versión larga". Sin embargo, ¿por qué esto afecta el hecho de que no ejecuto la parte del código en mi método run() en el momento de la Runnable objeto Runnable ? Todavía puedo hacer la inicialización antes de invocar run() . Así que técnicamente es posible no encontrar una NullPointerException aquí. (Aunque sería mejor verificar si aquí también hay null . Pero esta convención es otro tema).

¿Cuál es el error que cometo? ¿Qué se maneja tan diferente acerca de la lambda que influye en el uso de mi objeto de la manera en que lo hace?

Le agradezco cualquier explicación adicional.


Puede evitar el problema por

Runnable run = () -> { (this).obj.toString(); };

Esto se discutió durante el desarrollo de lambda, básicamente, el cuerpo de lambda se trata como un código local durante el análisis de la asignación definitiva .

Citando a Dan Smith, spec tzar, https://bugs.openjdk.java.net/browse/JDK-8024809

Las reglas establecen dos excepciones: ... ii) un uso desde dentro de una clase anónima está bien. No hay excepción para un uso dentro de una expresión lambda

Francamente, yo y otras personas pensamos que la decisión es incorrecta. La lambda solo captura this , no obj . Este caso debería haber sido tratado igual que la clase anónima. El comportamiento actual es problemático para muchos casos de uso legítimos. Bueno, siempre puede evitarlo utilizando el truco. Afortunadamente, el análisis de asignaciones definidas no es demasiado inteligente y podemos engañarlo.


Puedes usar un método de utilidad para forzar la captura de this solo. Esto funciona con Java 9 también.

public static <T> T r(T object) { return object; }

Ahora, puedes reescribir tu lambda así:

Runnable run = () -> r(this).obj.toString();