variable valor una paso pasar parametros otra metodos locales globales entre ejemplos ejemplo datos clase java lambda java-8 closures method-reference

java - valor - ¿Por qué la referencia de método puede usar variables no finales?



paso de parametros en java netbeans (4)

El consenso parece ser que esto se debe a que cuando lo haces usando una clase anónima, one refiere a una variable, mientras que cuando lo haces usando una referencia de método, el valor de one se captura cuando se crea el identificador del método. De hecho, creo que en ambos casos one es un valor en lugar de una variable. Consideremos las clases anónimas, expresiones lambda y referencias de métodos con un poco más de detalle.

Clases anónimas

Considere el siguiente ejemplo:

static Supplier<String> getStringSupplier() { final Object o = new Object(); return new Supplier<String>() { @Override public String get() { return o.toString(); } }; } public static void main(String[] args) { Supplier<String> supplier = getStringSupplier(); System.out.println(supplier.get()); // Use o after the getStringSupplier method returned. }

En este ejemplo, llamamos a toString en o después de que el método getStringSupplier haya vuelto, por lo que cuando aparece en el método get , o no puede hacer referencia a una variable local del método getStringSupplier . De hecho, es esencialmente equivalente a esto:

static Supplier<String> getStringSupplier() { final Object o = new Object(); return new StringSupplier(o); } private static class StringSupplier implements Supplier<String> { private final Object o; StringSupplier(Object o) { this.o = o; } @Override public String get() { return o.toString(); } }

Las clases anónimas hacen que parezca que está utilizando variables locales, cuando en realidad se capturan los valores de estas variables.

En contraste con esto, si un método de una clase anónima hace referencia a los campos de la instancia adjunta, los valores de estos campos no se capturan, y la instancia de la clase anónima no contiene referencias a ellos; en su lugar, la clase anónima contiene una referencia a la instancia adjunta y puede acceder a sus campos (ya sea directamente o mediante accesos sintéticos, dependiendo de la visibilidad). Una ventaja es que se requiere una referencia adicional a un solo objeto, en lugar de varios.

Expresiones Lambda

Las expresiones lambda también se cierran sobre los valores, no sobre las variables. La razón dada por Brian Goetz here es que

modismos como este:

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

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.

Referencias de métodos

El hecho de que las referencias a métodos capturen el valor de la variable cuando se crea el identificador del método es fácil de verificar.

Por ejemplo, el siguiente código imprime "a" dos veces:

String s = "a"; Supplier<String> supplier = s::toString; System.out.println(supplier.get()); s = "b"; System.out.println(supplier.get());

Resumen

Entonces, en resumen, las expresiones lambda y las referencias de método se cierran sobre los valores, no sobre las variables. Las clases anónimas también cierran los valores en el caso de las variables locales. En el caso de los campos, la situación es más complicada, pero el comportamiento es esencialmente el mismo que capturar los valores porque los campos deben ser efectivamente finales.

En vista de esto, la pregunta es, ¿por qué las reglas que se aplican a clases anónimas y expresiones lambda no se aplican a las referencias de métodos, es decir, por qué se le permite escribir o::toString cuando o no es efectivamente final? No sé la respuesta a eso, pero me parece una inconsistencia. Supongo que es porque no puedes hacer tanto daño con una referencia de método; ejemplos como el que se cita arriba para las expresiones lambda no se aplican.

Tenía cierta confusión sobre las clases internas y la expresión lambda, e intenté hacer una question respecto, pero luego surgió otra duda, y es probable que sea mejor publicar otra pregunta que comentar la anterior.

Directamente al grano: sé ( gracias Jon ) que algo como esto no compilará

public class Main { public static void main(String[] args) { One one = new One(); F f = new F(){ //1 public void foo(){one.bar();} //compilation error }; one = new One(); } } class One { void bar() {} } interface F { void foo(); }

debido a cómo Java maneja los cierres, porque one no es [efectivamente] final y así sucesivamente.

Pero entonces, ¿cómo es que esto está permitido?

public class Main { public static void main(String[] args) { One one = new One(); F f = one::bar; //2 one = new One(); } } class One { void bar() {} } interface F { void foo(); }

¿No es //2 equivalente a //1 ? ¿No estoy, en el segundo caso, enfrentando los riesgos de "trabajar con una variable desactualizada"?

Quiero decir, en este último caso, después de one = new One(); se ejecuta f aún tiene una copia desactualizada de one (es decir, hace referencia al objeto viejo). ¿No es este el tipo de ambigüedad que intentamos evitar?


No. En su primer ejemplo, define la implementación de F en línea e intenta acceder a la variable de instancia uno.

En el segundo ejemplo, básicamente define su expresión lambda como la llamada de bar() en el objeto uno.

Ahora esto puede ser un poco confuso. El beneficio de esta notación es que puede definir un método (la mayoría de las veces es un método estático o en un contexto estático) una vez y luego hacer referencia al mismo método desde varias expresiones lambda:

msg -> System.out::println(msg);


Su segundo ejemplo simplemente no es una expresión lambda. Es una referencia de método. En este caso particular, elige un método de un objeto particular, al que actualmente hace referencia la variable one . Pero la referencia es al objeto , no a la variable uno .

Esto es lo mismo que el caso clásico de Java:

One one = new One(); One two = one; one = new One(); two.bar();

¿Y qué si one cambió? two referencias al objeto que solía ser, y puede acceder a su método.

Su primer ejemplo, por otro lado, es una clase anónima, que es una estructura clásica de Java que puede referirse a variables locales a su alrededor. El código se refiere a la variable real, no al objeto al que se refiere. Esto está restringido por las razones que Jon mencionó en la respuesta a la que se refiere. Tenga en cuenta que el cambio en Java 8 es simplemente que la variable tiene que ser efectivamente definitiva . Es decir, todavía no se puede cambiar después de la inicialización. El compilador simplemente se volvió lo suficientemente sofisticado como para determinar qué casos no serán confusos incluso cuando el modificador final no se use explícitamente.


Una referencia de método no es una expresión lambda, aunque se pueden usar de la misma manera. Creo que eso es lo que está causando la confusión. A continuación se muestra una simplificación de cómo funciona Java, no es cómo realmente funciona, pero está lo suficientemente cerca.

Digamos que tenemos una expresión lambda:

Runnable f = () -> one.bar();

Esto es el equivalente de una clase anónima que implementa Runnable :

Runnable f = new Runnable() { public void run() { one.bar(); } }

Aquí se aplican las mismas reglas que para una clase anónima (o clase local de método). Esto significa que one necesita efectivamente final para que funcione.

Por otro lado, el método handle:

Runnable f = one::bar;

Es más como:

Runnable f = new MethodHandle(one, one.getClass().getMethod("bar"));

Con MethodHandle siendo:

public class MethodHandle implements Runnable { private final Object object; private final Method method; public MethodHandle(Object object, java.lang.reflect.Method method) { this.object = Object; this.method = method; } @Override public void run() { method.invoke(object); } }

En este caso, el objeto asignado a one se asigna como parte del identificador de método creado, por lo que one no necesita ser efectivamente final para que esto funcione.