在一个包(a
)中,我有两个功能接口:
package a;
@FunctionalInterface
interface Applicable<A extends Applicable<A>> {
void apply(A self);
}
-
package a;
@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
}
超级接口中的apply
方法将self
作为A
,否则,如果使用Applicable<A>
,则类型将无法在包外显示,因此该方法无法实施。
在另一个包(b
)中,我有以下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);
}
}
第一个实现使用匿名类,它没有问题。另一方面,第二个编译很好,但在运行时因java.lang.BootstrapMethodError
尝试访问java.lang.IllegalAccessError
接口而导致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
我认为如果lambda表达式像匿名类一样工作或者给出编译时错误会更有意义。所以,我只是想知道这里发生了什么。
我尝试删除超级接口并在SomeApplicable
中声明方法,如下所示:
package a;
@FunctionalInterface
public interface SomeApplicable {
void apply(SomeApplicable self);
}
这显然使其有效,但允许我们查看字节码中的不同之处。
从lambda表达式编译的合成lambda$0
方法在两种情况下看起来都相同,但我可以发现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
#59
从(La/Applicable;)V
更改为(La/SomeApplicable;)V
。
我真的不知道lambda metafactory是如何工作的,但我认为这可能是一个关键的区别。
我还尝试在apply
中明确声明SomeApplicable
这样的方法:
package a;
@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
@Override
void apply(SomeApplicable self);
}
现在方法apply(SomeApplicable)
实际存在,编译器为apply(Applicable)
生成桥接方法。在运行时仍会抛出相同的错误。
在字节码级别,它现在使用LambdaMetafactory.altMetafactory
而不是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
答案 0 :(得分:11)
据我所知,JVM做的一切都很正确。
在apply
中声明Applicable
方法,而在SomeApplicable
中声明{匿名类}时,不应该使用lambda。让我们检查字节码。
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
生成接口方法apply(Applicable)
的实现和覆盖方法apply(SomeApplicable)
。除了方法签名之外,这两种方法都没有引用不可访问的接口Applicable
。也就是说,Applicable
接口在匿名类的代码中的任何位置都没有解析(JVMS §5.4.3)。
请注意,apply(Applicable)
可以成功调用Test
,因为在解析invokeinterface
指令(JVMS §5.4.3.4)期间,方法签名中的类型无法解析。
通过使用引导方法invokedynamic
执行LambdaMetafactory.metafactory
字节码来获取lambda的实例:
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
用于构造lambda的静态参数是:
void (a.Applicable)
; void (a.SomeApplicable)
。所有这些参数都在invokedynamic
引导过程(JVMS §5.4.3.6)期间得到解决。
现在关键点:要解析MethodType,解析其方法描述符中给出的所有类和接口(JVMS §5.4.3.5)。特别是,JVM尝试代表a.Applicable
类解析Test
,并使用IllegalAccessError
失败。然后,根据invokedynamic
的规范,错误将包含在BootstrapMethodError
。
要解决IllegalAccessError
,您需要在可公开访问的SomeApplicable
界面中明确添加桥接方法:
public interface SomeApplicable extends Applicable<SomeApplicable> {
@Override
void apply(SomeApplicable self);
}
在这种情况下,lambda将实现apply(SomeApplicable)
方法而不是apply(Applicable)
。相应的invokedynamic
指令将引用(La/SomeApplicable;)V
MethodType,它将成功解析。
注意:仅更改SomeApplicable
接口是不够的。您必须使用新版本的Test
重新编译SomeApplicable
,才能使用正确的MethodTypes生成invokedynamic
。我已经在从8u31到最新的9-ea的几个JDK上验证了这一点,并且所讨论的代码没有错误地工作。