Lambda表达式在运行时出现java.lang.BootstrapMethodError错误

22

在一个包(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,由于试图访问 Applicable 接口而引发了一个 java.lang.IllegalAccessError

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


我还尝试了像这样显式声明SomeApplicable中的apply方法:

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

请提供完整的堆栈跟踪信息,抛出 Error 听起来非常可疑。 - GhostCat
2
根据你的描述,我不确定那个“DUP”是合法的。如果我是你,我会创建一个完整的最小可行示例,并将其放入你的问题中。如果你可以展示出一个编译出错的代码片段,编译自一个文件里,那么这个"DUP"就不匹配;然后你应该要求重新打开。 - GhostCat
2
@GhostCat 我认为如果没有两个包,就不可能出现这个错误,超级接口必须不可见。 - Bubletan
3
我认为多文件 MCVEs 是可以接受的 - 重要的是 最小化 部分... 最小化并不一定意味着单个文件,但它确实意味着“不要填满我的浏览器缓存”。 - dcsohl
1
我会重新打开。我可以使用javac复现,但不能使用Eclipse,可能是bug。 - Sotirios Delimanolis
显示剩余4条评论
1个回答

12

就我所见,JVM 做得非常好。

当在 Applicable 中声明 apply 方法但在 SomeApplicable 中没有时,匿名类应该可以工作,而 lambda 表达式则不行。让我们来检查一下字节码。

匿名类 Test$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 生成接口方法实现 apply(Applicable) 和重写方法 apply(SomeApplicable)。这两个方法都不涉及无法访问的接口 Applicable,仅在方法签名中引用该接口。也就是说,在匿名类代码中没有任何地方解析 Applicable 接口(JVMS §5.4.3)。

请注意,apply(Applicable) 可以在 Test 中成功调用,因为方法签名中的类型在解析 invokeinterface 指令时未被解析(JVMS §5.4.3.4)。

Lambda

lambda 的实例通过执行带有引导方法 LambdaMetafactory.metafactoryinvokedynamic 字节码获得:

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. 实现接口的MethodType:void (a.Applicable)
  2. 实现方法的直接MethodHandle;
  3. lambda表达式的有效MethodType:void (a.SomeApplicable)

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

现在关键点是:为了解析MethodType,需要解析其方法描述符中给出的所有类和接口 (JVMS §5.4.3.5)。特别地,JVM试图代表Test类解析a.Applicable,但由于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接口是不够的。您需要使用新版本的SomeApplicable重新编译Test,以便生成具有正确MethodTypes的invokedynamic。我在从8u31到最新的9-ea的几个JDK上进行了验证,并且相关的代码可以正常工作而没有错误。


2
桥接方法解决方法似乎在使用Eclipse编译时无效。现在我尝试使用javac编译,结果如预期一样工作。由于某种原因,Eclipse编译器使用带有“BRIDGES”标志(以及“(La/Applicable;)V”作为桥接方法类型)的altMetafactory,这会导致发生相同的错误。我想出的另一个简单解决方法是将“Applicable”声明为“Applicable <A extends Object&Applicable <A>>”。然后参数类型将变为公共类型,因为它被擦除为“Object”。 - Bubletan
无论如何,如果JVM一切正常,那么在我看来这似乎是编译器的问题。给出一个错误或使用一些可能的解决方法比默默地接受非法代码要好得多。 - Bubletan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接