Lambda 表达式在运行时失败,并带有 java.lang.BootstrapMethodError

2022-09-02 01:57:23

在一个包()中,我有两个功能接口:a

package a;

@FunctionalInterface
interface Applicable<A extends Applicable<A>> {

    void apply(A self);
}

-

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {
}

超接口中的方法采用 a,因为否则,如果改用,则类型在包外部不可见,因此无法实现该方法。applyselfAApplicable<A>

在另一个包()中,我有以下类:bTest

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.BootstrapMethodErrorjava.lang.IllegalAccessErrorApplicable

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 表达式编译的合成方法在这两种情况下似乎完全相同,但我可以在 bootstrap 方法下的方法参数中发现一个差异。lambda$0

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是如何工作的,但我认为这可能是一个关键的区别。


我还尝试像这样显式声明该方法:applySomeApplicable

package a;

@FunctionalInterface
public interface SomeApplicable extends Applicable<SomeApplicable> {

    @Override
    void apply(SomeApplicable self);
}

现在该方法实际存在,编译器为 生成一个桥接方法。在运行时仍会引发相同的错误。apply(SomeApplicable)apply(Applicable)

在字节码级别,它现在使用而不是:LambdaMetafactory.altMetafactoryLambdaMetafactory.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

答案 1

据我所知,JVM做对了一切。

当 方法在 中声明,但不在 中声明时,匿名类应该工作,而 lambda 不应该工作。让我们检查一下字节码。applyApplicableSomeApplicable

匿名类测试$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生成接口方法和重写方法的实现。这两种方法都不涉及不可访问的接口,除了方法签名。也就是说,在匿名类代码中的任何位置都无法解析接口(JVMS §5.4.3)。apply(Applicable)apply(SomeApplicable)ApplicableApplicable

请注意,可以从 成功调用,因为在指令解析期间不会解析方法签名中的类型 (JVMS §5.4.3.4)。apply(Applicable)Testinvokeinterface

Lambda

lambda 的实例是通过使用 bootstrap 方法 LambdaMetafactory.metafactory 执行 invokedynamic 字节码来获得的:

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 的静态参数包括:

  1. 实现的接口的方法类型:void (a.Applicable);
  2. 直接方法处理到实现;
  3. lambda 表达式的有效方法类型:。void (a.SomeApplicable)

所有这些参数都在引导过程中得到解决(JVMS §5.4.3.6)。invokedynamic

现在的关键点:要解析一个MethodType,其方法描述符中给出的所有类和接口都被解析了(JVMS §5.4.3.5)。特别是,JVM 尝试代表类进行解析,但失败。然后,根据调用动力学的规范,将错误包装到 .a.ApplicableTestIllegalAccessErrorBootstrapMethodError

桥接方法

要变通方法,您需要在可公开访问的接口中显式添加一个 bridge 方法:IllegalAccessErrorSomeApplicable

public interface SomeApplicable extends Applicable<SomeApplicable> {
    @Override
    void apply(SomeApplicable self);
}

在这种情况下,lambda 将实现方法而不是 。相应的指令将引用方法类型,该方法将成功解析。apply(SomeApplicable)apply(Applicable)invokedynamic(La/SomeApplicable;)V

注意:仅仅改变界面是不够的。您必须使用 新版本重新编译,才能使用正确的方法类型进行生成。我已经在从8u31到最新的9-ea的几个JDK上验证了这一点,并且有问题的代码没有错误。SomeApplicableTestSomeApplicableinvokedynamic


答案 2

推荐