【问题标题】:Lambda expression fails with a java.lang.BootstrapMethodError at runtimeLambda 表达式在运行时失败并出现 java.lang.BootstrapMethodError
【发布时间】:2017-03-09 07:09:36
【问题描述】:

在一个包中 (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&lt;A&gt;,则该类型在包外将不可见,因此无法实现该方法。

在另一个包 (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);
    }
}

第一个实现使用匿名类,它可以正常工作。另一方面,第二个编译正常,但在运行时失败,在尝试访问Applicable 接口时抛出由java.lang.IllegalAccessError 引起的java.lang.BootstrapMethodError

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


我还尝试像这样在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 听起来很可疑。
  • 根据您的描述,我不确定“DUP”关闭是否合法。如果我是您,我会创建一个完整的最小可行示例并将其放入您的问题中。如果您可以显示 one 段代码,编译出 one 文件导致此错误,则 DUP 不匹配;你应该要求重新打开。
  • @GhostCat 我认为没有两个包是不可能得到这个错误的,超级接口一定是不可见的。
  • 我认为多文件 MCVE 是可以接受的 - 重要的部分是 minimal 部分...最小不一定意味着单文件,但它确实意味着“不要填充我的浏览器缓存”。
  • 我会重新打开。我可以用javac 重现,而不是用 Eclipse,也许是 bug。

标签: java generics lambda package-private


【解决方案1】:

据我所知,JVM 做的一切都是正确的。

apply 方法在Applicable 中声明但不在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)时没有被解析。

拉姆达

通过使用引导方法LambdaMetafactory.metafactory执行invokedynamic字节码获得一个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 的静态参数是:

  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 上验证了这一点,并且有问题的代码可以正常工作。

【讨论】:

  • 如果使用 Eclipse 编译,桥接方法解决方法似乎不起作用。现在我用 javac 尝试了它,它按预期工作。由于某种原因,Eclipse 编译器使用altMetafactoryBRIDGES 标志(以及(La/Applicable;)V 作为桥方法类型),这会导致发生相同的错误。我想出的另一个简单的解决方法是将Applicable 声明为Applicable&lt;A extends Object &amp; Applicable&lt;A&gt;&gt;。然后参数类型将是公共的,因为它被删除到Object
  • 无论如何,如果 JVM 一切正常,对我来说它看起来像是一个编译器问题。给出错误或使用一些可能的解决方法将是比仅仅默默接受非法代码更好的选择。
猜你喜欢
  • 1970-01-01
  • 2018-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多