funciones expresiones explicacion ejemplos ejemplo anonimas java serialization lambda java-8

java - expresiones - ¿Cuál es la diferencia entre una referencia lambda y un método a nivel de tiempo de ejecución?



java 8 lambda explicacion (2)

Empezando

Para investigar esto comenzamos con la siguiente clase:

import java.io.Serializable; import java.util.Comparator; public final class Generic { // Bad implementation, only used as an example. public static final Comparator<Integer> COMPARATOR = (a, b) -> (a > b) ? 1 : -1; public static Comparator<Integer> reference() { return (Comparator<Integer> & Serializable) COMPARATOR::compare; } public static Comparator<Integer> explicit() { return (Comparator<Integer> & Serializable) (a, b) -> COMPARATOR.compare(a, b); } }

Después de la compilación, podemos desmontarlo usando:

javap -c -p -s -v Generic.class

Eliminar las partes irrelevantes (y algún otro desorden, como los tipos totalmente calificados y la inicialización de COMPARATOR ) nos queda con

public static final Comparator<Integer> COMPARATOR; public static Comparator<Integer> reference(); 0: getstatic #2 // Field COMPARATOR:LComparator; 3: dup 4: invokevirtual #3 // Method Object.getClass:()LClass; 7: pop 8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator; 13: checkcast #5 // class Serializable 16: checkcast #6 // class Comparator 19: areturn public static Comparator<Integer> explicit(); 0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator; 5: checkcast #5 // class Serializable 8: checkcast #6 // class Comparator 11: areturn private static int lambda$explicit$d34e1a25$1(Integer, Integer); 0: getstatic #2 // Field COMPARATOR:LComparator; 3: aload_0 4: aload_1 5: invokeinterface #44, 3 // InterfaceMethod Comparator.compare:(LObject;LObject;)I 10: ireturn BootstrapMethods: 0: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite; Method arguments: #62 (LObject;LObject;)I #63 invokeinterface Comparator.compare:(LObject;LObject;)I #64 (LInteger;LInteger;)I #65 5 #66 0 1: #61 invokestatic invoke/LambdaMetafactory.altMetafactory:(Linvoke/MethodHandles$Lookup;LString;Linvoke/MethodType;[LObject;)Linvoke/CallSite; Method arguments: #62 (LObject;LObject;)I #70 invokestatic Generic.lambda$explicit$df5d232f$1:(LInteger;LInteger;)I #64 (LInteger;LInteger;)I #65 5 #66 0

Inmediatamente vemos que el bytecode para el método reference() es diferente al bytecode para explicit() . Sin embargo, la diferencia notable no es realmente relevante , pero los métodos de arranque son interesantes.

Un sitio de llamada dinámico invocado se vincula a un método por medio de un método de arranque , que es un método especificado por el compilador para el lenguaje de tipo dinámico que JVM llama una vez para vincular el sitio.

( Soporte de máquina virtual Java para lenguajes no Java , énfasis en el suyo)

Este es el código responsable de crear el CallSite utilizado por lambda. Los Method arguments enumeran debajo de cada método de arranque son los valores pasados ​​como parámetro variable (es decir, args ) de LambdaMetaFactory#altMetaFactory .

Formato de los argumentos del Método

  1. samMethodType: firma y tipo de método de retorno que implementará el objeto de función.
  2. implMethod: un identificador de método directo que describe el método de implementación que debe llamarse (con una adaptación adecuada de los tipos de argumento, tipos de retorno y con argumentos capturados antepuestos a los argumentos de invocación) en el momento de la invocación.
  3. instantiatedMethodType: el tipo de firma y devolución que se debe aplicar dinámicamente en el momento de la invocación. Esto puede ser lo mismo que samMethodType, o puede ser una especialización de este.
  4. banderas indica opciones adicionales; Este es un OR bit a bit de las banderas deseadas. Las banderas definidas son FLAG_BRIDGES, FLAG_MARKERS y FLAG_SERIALIZABLE.
  5. bridgeCount es el número de firmas de métodos adicionales que debe implementar el objeto de función, y está presente si y solo si se establece el indicador FLAG_BRIDGES.

En ambos casos, bridgeCount es 0, por lo que no hay 6, que de otro modo serían bridges : una lista de longitud variable de firmas de métodos adicionales para implementar (dado que bridgeCount es 0, no estoy completamente seguro de por qué FLAG_BRIDGES está configurado) .

Emparejando lo anterior con nuestros argumentos, obtenemos:

  1. La firma de la función y el tipo de retorno (Ljava/lang/Object;Ljava/lang/Object;)I , que es el tipo de retorno de Comparator#compare , debido a la (Ljava/lang/Object;Ljava/lang/Object;)I del tipo genérico.
  2. El método que se llama cuando se invoca esta lambda (que es diferente).
  3. La firma y el tipo de retorno del lambda, que se comprobará cuando se invoque el lambda: (LInteger;LInteger;)I (tenga en cuenta que estos no se borran, porque esto es parte de la especificación lambda).
  4. Las banderas, que en ambos casos son la composición de FLAG_BRIDGES y FLAG_SERIALIZABLE (es decir, 5).
  5. La cantidad de firmas de método de puente, 0.

Podemos ver que FLAG_SERIALIZABLE está configurado para ambas lambdas, por lo que no es eso.

Métodos de implementación

El método de implementación para la referencia de método lambda es Comparator.compare:(LObject;LObject;)I , pero para el lambda explícito es Generic.lambda$explicit$df5d232f$1:(LInteger;LInteger;)I Mirando el desmontaje, podemos ver que el primero es esencialmente una versión en línea del segundo. La única otra diferencia notable son los tipos de parámetros del método (que, como se mencionó anteriormente, se debe a la eliminación del tipo genérico).

¿Cuándo se puede serializar una lambda?

Puede serializar una expresión lambda si su tipo de destino y sus argumentos capturados son serializables.

Expresiones Lambda (Los Tutoriales de Java ™)

La parte importante de eso es "argumentos capturados". Mirando hacia atrás al bytecode desmontado, la instrucción invocada dinámica para la referencia del método ciertamente parece que está capturando un Comparator ( #0:compare:(LComparator;)LComparator; en contraste con el lambda explícito, #1:compare:()LComparator; )

Confirmar la captura es el problema

ObjectOutputStream contiene un campo extendedDebugInfo , que podemos establecer usando el -Dsun.io.serialization.extendedDebugInfo=true VM:

$ java -Dsun.io.serialization.extendedDebugInfo = verdadero Genérico

Cuando intentamos serializar las lambdas nuevamente, esto da un resultado muy satisfactorio.

Exception in thread "main" java.io.NotSerializableException: Generic$$Lambda$1/321001045 - element of array (index: 0) - array (class "[LObject;", size: 1) /* ! */ - field (class "invoke.SerializedLambda", name: "capturedArgs", type: "class [LObject;") // <--- !! - root object (class "invoke.SerializedLambda", SerializedLambda[capturingClass=class Generic, functionalInterfaceMethod=Comparator.compare:(LObject;LObject;)I, implementation=invokeInterface Comparator.compare:(LObject;LObject;)I, instantiatedMethodType=(LInteger;LInteger;)I, numCaptured=1]) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1182) /* removed */ at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at Generic.main(Generic.java:27)

¿Qué está pasando realmente?

De lo anterior, podemos ver que el lambda explícito no está capturando nada, mientras que el método de referencia lambda sí. Al revisar el bytecode nuevamente, esto queda claro:

public static Comparator<Integer> explicit(); 0: invokedynamic #7, 0 // InvokeDynamic #1:compare:()LComparator; 5: checkcast #5 // class java/io/Serializable 8: checkcast #6 // class Comparator 11: areturn

Que, como se ve arriba, tiene un método de implementación de:

private static int lambda$explicit$d34e1a25$1(java.lang.Integer, java.lang.Integer); 0: getstatic #2 // Field COMPARATOR:Ljava/util/Comparator; 3: aload_0 4: aload_1 5: invokeinterface #44, 3 // InterfaceMethod java/util/Comparator.compare:(Ljava/lang/Object;Ljava/lang/Object;)I 10: ireturn

El lambda explícito en realidad está llamando al lambda$explicit$d34e1a25$1 , que a su vez llama al COMPARATOR#compare . Esta capa de indirección significa que no está capturando nada que no sea Serializable (o cualquier cosa, para ser precisos), por lo que es seguro serializar. La expresión de referencia del método usa directamente COMPARATOR (cuyo valor luego se pasa al método bootstrap):

public static Comparator<Integer> reference(); 0: getstatic #2 // Field COMPARATOR:LComparator; 3: dup 4: invokevirtual #3 // Method Object.getClass:()LClass; 7: pop 8: invokedynamic #4, 0 // InvokeDynamic #0:compare:(LComparator;)LComparator; 13: checkcast #5 // class java/io/Serializable 16: checkcast #6 // class Comparator 19: areturn

La falta de indirección significa que COMPARATOR debe ser serializado junto con la lambda. Como COMPARATOR no se refiere a un valor Serializable , esto falla.

La solución

Dudo en llamar a esto un error del compilador (espero que la falta de indirección sirva como una optimización), aunque es muy extraño. La solución es trivial, pero fea; agregando el reparto explícito para COMPARATOR en la declaración:

public static final Comparator<Integer> COMPARATOR = (Serializable & Comparator<Integer>) (a, b) -> a > b ? 1 : -1;

Esto hace que todo funcione correctamente en Java 1.8.0_45. También vale la pena señalar que el compilador de eclipse también produce esa capa de indirección en el caso de referencia del método, por lo que el código original en esta publicación no requiere modificación para ejecutarse correctamente.

Experimenté un problema que estaba sucediendo usando una referencia de método pero no con lambdas. Ese código fue el siguiente:

(Comparator<ObjectNode> & Serializable) SOME_COMPARATOR::compare

o con lambda

(Comparator<ObjectNode> & Serializable) (a, b) -> SOME_COMPARATOR.compare(a, b)

Semánticamente, es estrictamente lo mismo, pero en la práctica es diferente, ya que en el primer caso obtengo una excepción en una de las clases de serialización de Java. Mi pregunta no es sobre esta excepción, porque el código real se está ejecutando en un contexto más complicado que ha demostrado tener un comportamiento extraño con la serialización, por lo que sería muy difícil responder si di más detalles.

Lo que quiero entender es la diferencia entre esas dos formas de crear una expresión lambda.


Quiero agregar el hecho de que en realidad hay una diferencia semántica entre un lambda y una referencia de método a un método de instancia (incluso cuando tienen el mismo contenido que en su caso, y sin tener en cuenta la serialización):

SOME_COMPARATOR::compare

Este formulario se evalúa como un objeto lambda que se cierra sobre el valor de SOME_COMPARATOR en el momento de la evaluación (es decir, contiene referencia a ese objeto). Verificará si SOME_COMPARATOR es nulo en el momento de la evaluación y arrojará una excepción de puntero nulo ya entonces. No recogerá los cambios en el campo que se realizan después de su creación.

(a,b) -> SOME_COMPARATOR.compare(a,b)

Este formulario se evalúa como un objeto lambda que accederá al valor del campo SOME_COMPARATOR cuando se lo llame . Está cerrado sobre this , ya que SOME_COMPARATOR es un campo de instancia. Cuando se lo llama, accederá al valor actual de SOME_COMPARATOR y lo usará, lo que posiblemente SOME_COMPARATOR una excepción de puntero nulo en ese momento.

Demostración

Este comportamiento se puede ver en el siguiente pequeño ejemplo. Al detener el código en un depurador e inspeccionar los campos de las lambdas, se puede verificar qué están cerradas.

Object o = "First"; void run() { Supplier<String> ref = o::toString; Supplier<String> lambda = () -> o.toString(); o = "Second"; System.out.println("Ref: " + ref.get()); // Prints "First" System.out.println("Lambda: " + lambda.get()); // Prints "Second" }

Especificación del lenguaje Java

El JLS describe este comportamiento de las referencias de métodos en 15.13.3 :

La referencia de destino es el valor de ExpressionName o Primary, según lo determinado cuando se evaluó la expresión de referencia del método.

Y:

Primero, si la expresión de referencia del método comienza con ExpressionName o Primary, se evalúa esta subexpresión. Si la subexpresión se evalúa como null , se NullPointerException una NullPointerException

En el código de Toby

Esto se puede ver en la lista de Tobys del código de reference , donde se getClass en el valor de SOME_COMPARATOR que desencadenará una excepción si es nulo:

4: invokevirtual #3 // Method Object.getClass:()LClass;

(O eso creo, realmente no soy un experto en código de bytes).

Sin embargo, las referencias a métodos en el código que se cumple con Eclipse 4.4.1 no arrojan una excepción en esa situación. Eclipse parece tener un error aquí.