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
- samMethodType: firma y tipo de método de retorno que implementará el objeto de función.
- 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.
- 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.
- 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.
- 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:
-
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. - El método que se llama cuando se invoca esta lambda (que es diferente).
-
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). - Las banderas, que en ambos casos son la composición de FLAG_BRIDGES y FLAG_SERIALIZABLE (es decir, 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
, seNullPointerException
unaNullPointerException
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í.