java - interfaces - porque usar lambda aws
La expresión Lambda falla con un java.lang.BootstrapMethodError en tiempo de ejecución (1)
Por lo que veo, JVM hace todo bien.
Cuando el método de apply
se declara en Applicable
, pero no en SomeApplicable
, la clase anónima debería funcionar, y la lambda no debería. Vamos a examinar el bytecode.
Prueba de clase anónima $ 1
public void apply(a.SomeApplicable);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String a
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void apply(a.Applicable);
Code:
0: aload_0
1: aload_1
2: checkcast #5 // class a/SomeApplicable
5: invokevirtual #6 // Method apply:(La/SomeApplicable;)V
8: return
javac
genera tanto la implementación del método de interfaz apply(Applicable)
como el método de invalidación apply(SomeApplicable)
. Ninguno de los métodos se refiere a una interfaz inaccesible. Applicable
, excepto en la firma del método. Es decir, la interfaz Applicable
no se resuelve (JVMS §5.4.3) en ninguna parte del código de la clase anónima.
Tenga en cuenta que se puede llamar con éxito a apply(Applicable)
desde Test
, porque los tipos en la firma del método no se resuelven durante la resolución de la instrucción invokeinterface (JVMS §5.4.3.4) .
Lambda
Una instancia de lambda se obtiene mediante la ejecución de un invokedynamic
byte invokedynamic
con el método de arranque LambdaMetafactory.metafactory
:
BootstrapMethods:
0: #36 invokestatic java/lang/invoke/LambdaMetafactory.metafactory
Method arguments:
#37 (La/Applicable;)V
#38 invokestatic b/Test.lambda$main$0:(La/SomeApplicable;)V
#39 (La/SomeApplicable;)V
Los argumentos estáticos utilizados para construir lambda son:
- MethodType de la interfaz implementada:
void (a.Applicable)
; - Dirigir el Método Manejar a la implementación;
- Tipo de método efectivo de la expresión lambda:
void (a.SomeApplicable)
.
Todos estos argumentos se resuelven durante el proceso de arranque dinámico invocado (JVMS §5.4.3.6) .
Ahora el punto clave: para resolver un MethodType se resuelven todas las clases e interfaces dadas en su descriptor de método (JVMS §5.4.3.5) . En particular, JVM intenta resolver un. a.Applicable
en nombre de la clase de Test
y falla con IllegalAccessError
. Luego, de acuerdo con la especificación de invokedynamic
, el error se envuelve en BootstrapMethodError
.
Método de puente
Para IllegalAccessError
, debe agregar explícitamente un método de puente en la interfaz SomeApplicable
accesible al SomeApplicable
:
public interface SomeApplicable extends Applicable<SomeApplicable> {
@Override
void apply(SomeApplicable self);
}
En este caso, lambda implementará el método apply(SomeApplicable)
lugar de apply(Applicable)
. La correspondiente instrucción invokedynamic
se referirá a (La/SomeApplicable;)V
MethodType, que se resolverá con éxito.
Nota: no es suficiente cambiar solo la interfaz de SomeApplicable
. Tendrá que SomeApplicable
a compilar la Test
con la nueva versión de SomeApplicable
para generar una invokedynamic
con los MethodTypes adecuados. He verificado esto en varios JDK desde 8u31 hasta el último 9-ea, y el código en cuestión funcionó sin errores.
En un paquete ( a
) tengo dos interfaces funcionales:
package a;
@FunctionalInterface
interface Applicable<A extends Applicable<A>> {
void apply(A self);
}
-
package a;
@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
}
El método de apply
en la superinterfaz se toma a self
como una A
porque, de lo contrario, si se utilizara Applicable<A>
, el tipo no sería visible fuera del paquete y, por lo tanto, el método no podría implementarse.
En otro paquete ( b
), tengo la siguiente clase de Test
:
package b;
import a.SomeApplicable;
public class Test {
public static void main(String[] args) {
// implement using an anonymous class
SomeApplicable a = new SomeApplicable() {
@Override
public void apply(SomeApplicable self) {
System.out.println("a");
}
};
a.apply(a);
// implement using a lambda expression
SomeApplicable b = (SomeApplicable self) -> System.out.println("b");
b.apply(b);
}
}
La primera implementación utiliza una clase anónima y funciona sin problemas. El segundo, por otro lado, compila bien pero falla en el tiempo de ejecución al lanzar un java.lang.BootstrapMethodError
causado por un java.lang.IllegalAccessError
cuando intenta acceder a la interfaz Applicable
.
Exception in thread "main" java.lang.BootstrapMethodError: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test
at b.Test.main(Test.java:19)
Caused by: java.lang.IllegalAccessError: tried to access class a.Applicable from class b.Test
... 1 more
Creo que tendría más sentido si la expresión lambda funcionara igual que la clase anónima o cometiera un error de compilación. Entonces, me pregunto qué está pasando aquí.
Intenté quitar la superinterfaz y declarar el método dentro de SomeApplicable
como esto:
package a;
@FunctionalInterface
public interface SomeApplicable {
void apply(SomeApplicable self);
}
Obviamente, esto hace que funcione, pero nos permite ver qué hay de diferente en el código de bytes.
El método lambda$0
sintético de lambda$0
compilado a partir de la expresión lambda parece idéntico en ambos casos, pero podría detectar una diferencia en los argumentos del método bajo los métodos bootstrap.
Bootstrap methods:
0 : # 58 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#59 (La/Applicable;)V
#62 invokestatic b/Test.lambda$0:(La/SomeApplicable;)V
#63 (La/SomeApplicable;)V
El #59
cambia de (La/Applicable;)V
a (La/SomeApplicable;)V
Realmente no sé cómo funciona la metafactor lambda, pero creo que esto podría ser una diferencia clave.
También intenté declarar explícitamente el método de apply
en SomeApplicable
como esta:
package a;
@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
@Override
void apply(SomeApplicable self);
}
Ahora el método apply(SomeApplicable)
realmente existe y el compilador genera un método de bridge para apply(Applicable)
. Todavía el mismo error se produce en tiempo de ejecución.
A nivel de bytecode ahora usa LambdaMetafactory.altMetafactory
lugar de LambdaMetafactory.metafactory
:
Bootstrap methods:
0 : # 57 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#58 (La/SomeApplicable;)V
#61 invokestatic b/Test.lambda$0:(La/SomeApplicable;)V
#62 (La/SomeApplicable;)V
#63 4
#64 1
#66 (La/Applicable;)V