【问题标题】:Duplicate methods generated for a lambda with Eclipse compiler使用 Eclipse 编译器为 lambda 生成重复方法
【发布时间】:2014-09-07 09:33:40
【问题描述】:

这一行

((UnaryOperator<Integer>)o->o).toString();

在类中的任何位置编写并使用 Eclipse Kepler 编译,在执行时到达该行时会导致失败:

java.lang.BootstrapMethodError: call site initialization exception
at java.lang.invoke.CallSite.makeSite(CallSite.java:328)
at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:296)
at test.Test.main(Test.java:7)
Caused by: java.lang.ClassFormatError: Duplicate method name&signature in class file test/Test$$Lambda$1
at sun.misc.Unsafe.defineAnonymousClass(Native Method)
at java.lang.invoke.InnerClassLambdaMetafactory.spinInnerClass(InnerClassLambdaMetafactory.java:324)
at java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:194)
at java.lang.invoke.LambdaMetafactory.altMetafactory(LambdaMetafactory.java:474)
at java.lang.invoke.CallSite.makeSite(CallSite.java:301)
... 2 more

就其本身而言,这并没有什么特别值得注意的,它是 Eclipse 的 Java 8 编译器中的另一个错误。但是,我对失败的细节很感兴趣。如果我们启用 jdk.internal.lambda.dumpProxyClasses 系统属性并检索生成的 lambda 类代码,使用 javap 对其进行解析将显示该类定义了两个相同的 apply 方法,其中一个标记为 bridge 方法:

{
  public java.lang.Object apply(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: checkcast     #14                 // class java/lang/Integer
         4: invokestatic  #20                 // Method test/Test.lambda$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
         7: areturn

  public java.lang.Object apply(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: checkcast     #14                 // class java/lang/Integer
         4: invokestatic  #20                 // Method test/Test.lambda$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
         7: areturn
}

我了解泛型需要桥接方法以保持向后兼容性;但是,我无法理解 Eclipse 中的错误如何迫使 JDK 合成有缺陷的方法对。

为了比较,如果我们将 Java 行稍微更改为:

((Object)((UnaryOperator<Integer>)o->o)).toString();

然后我们只得到一个非桥接方法,并且没有失败:

{
  public java.lang.Object apply(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: checkcast     #14                 // class java/lang/Integer
         4: invokestatic  #20                 // Method test/Test.lambda$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
         7: areturn
}

难道这其实是JDK的一个bug,却不是javac惹的祸?

我在 OS X 和带有 Java 8 补丁的 Eclipse Kepler SR2 上使用 javac 1.8.0_20

更新:引导方法调用

Eclipse 编译器负责发出正确的invokedynamic 引导方法调用(lambda 元工厂)。这是失败案例的引导方法参数的样子:

BootstrapMethods:
      0: #39 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:
          #41 (Ljava/lang/Object;)Ljava/lang/Object;
          #44 invokestatic test/Test.lambda$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
          #45 (Ljava/lang/Integer;)Ljava/lang/Integer;
          #46 4
          #47 1
          #48 (Ljava/lang/Object;)Ljava/lang/Object;

在 Brian 的帮助下,我现在很清楚上面的最后两行会导致错误:

  • #47 上的数字 1 表示“只有一种桥接方法”;
  • #48 上的(Ljava/lang/Object;)Ljava/lang/Object; 描述了桥接方法的签名,显然与主签名相同。

为了比较,这是可行的情况:

BootstrapMethods:
      0: #53 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:
          #55 (Ljava/lang/Object;)Ljava/lang/Object;
          #58 invokestatic test/Test.lambda$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
          #59 (Ljava/lang/Integer;)Ljava/lang/Integer;

这里使用了更简单的metafactory 方法,并且没有创建桥接方法。

【问题讨论】:

    标签: java eclipse lambda java-8


    【解决方案1】:

    根据您发布的堆栈跟踪,这几乎可以肯定是 Eclipse 代码生成中的错误,而不是 JDK。你可以从javap 的代码列表中找到这一点它处理不寻常的情况,例如可序列化的 lambda、由 lambda 对象实现的附加标记接口或目标接口未处理的桥接方法。

    作为参考,唯一明确需要额外桥梁的情况是

    1. 当目标接口需要桥接但使用较旧的 javac 编译,因此桥接不存在于接口本身中时,或
    2. 当目标类型和其他接口之间的交互需要桥接时。

    两者都是严重的极端情况,应该极少发生。

    【讨论】:

    • 是的,我已经确定使用了altMetaFactory,并且仅在失败的情况下使用。我仍然找不到提供方法实现的代码;我什至不知道在哪里寻找那个。它在常量池中吗?此外,我希望 lambda 工厂机制足够高级,不允许创建无效类。基本上,我试图深入了解 lambda (meta)factory 的内部运作。
    • 如果您将静态引导参数的 javap 列表(常量池中的引导方法表,javap -v)发布到 altMetaFactory 引导,我可以告诉您它应该 说。引导方法列表紧跟在javap -v -c 列表中的常量池之后;通过查看捕获所需 lambda 的 invokedynamic 指令,在表中找到正确的条目。
    • 我已经粘贴了,但我已经可以感谢您回答我的问题了。显然,问题发生在标记为#48 的条目上。我已经与altMetaFactory 的签名进行了交叉核对,这是“桥梁”论点。
    • 是的,eclipse 要求一个冗余的网桥。
    • 这从未向 Eclipse 报告过。现在试图关闭循环,我找不到任何已发布的 ecj 版本,它为此示例创建涉及 altMetaFactory 的代码。听起来 ecj 和 javac 从 Java 8 GA 开始就在这里达成一致(除非示例中的内容比上面显示的更多)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-12
    • 1970-01-01
    • 2010-09-17
    • 2016-12-15
    • 1970-01-01
    • 2023-03-22
    相关资源
    最近更新 更多