13

让我们使用 Eclipse Mars.2 包中的 ECJ 编译器编译以下代码:

import java.util.stream.*;

public class Test {
    String test(Stream<?> s) {
        return s.collect(Collector.of(() -> "", (a, t) -> {}, (a1, a2) -> a1));
    }
}

编译命令如下:

$ java -jar org.eclipse.jdt.core_3.11.2.v20160128-0629.jar -8 -g Test.java

编译成功后,让我们检查生成的类文件javap -v -p Test.class。最有趣的是为(a, t) -> {}lambda 生成的合成方法:

  private static void lambda$1(java.lang.String, java.lang.Object);
    descriptor: (Ljava/lang/String;Ljava/lang/Object;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0     a   Ljava/lang/String;
            0       1     1     t   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       1     1     t   !*

!*看到这个条目我很惊讶LocalVariableTypeTable。JVM 规范涵盖LocalVariableTypeTable 属性并说:

constant_pool索引处的条目必须包含一个CONSTANT_Utf8_info结构(第 4.4.7 节),该结构表示一个字段签名,该签名对源程序中的局部变量的类型进行编码(第 4.7.9.1 节)。

§4.7.9.1定义了字段签名的语法,如果我理解正确,它不涵盖任何类似于!*.

还应该注意的是,javac 编译器和较早的 ECJ 3.10.x 版本都不会生成此条LocalVariableTypeTable目。是!*一些非标准的 Eclipse 扩展还是我在 JVM 规范中遗漏了一些东西?这是否意味着 ECJ 不符合 JVM 规范?实际上是什么!*意思,是否有任何其他类似的字符串可能出现在LocalVariableTypeTable属性中?

4

1 回答 1

7

ecj 使用该令牌!对通用签名中的捕获类型进行编码。因此!*表示捕获了无界通配符。

在内部,ecj 使用两种风格CaptureBinding,一种用于实现JLS 18.4所称的“新鲜类型变量”,另一种用于实现捕获 JLS 5.1.10(它使用相同的“自由类型变量”术语)。两者都使用!. 仔细观察,在这个例子中,我们有一个“老式”的捕获:thas type capture#1-of ?,捕获<T>in Stream<T>

问题是:JVMS 4.7.9.1。似乎没有为此类新类型变量定义编码(在其他属性中,源代码中没有对应关系,因此没有名称)。

我无法为 lambdajavac发出任何LocalVariableTypeTable信息,因此他们可能会简单地避免回答这个问题。

鉴于两个编译器都同意推断t捕获,为什么一个编译器生成 LVTT,而另一个不生成?JVMS 4.7.14有这个

这种差异仅对类型使用类型变量或参数化类型的变量显着。

根据 JLS,捕获是新的类型变量,因此 LVTT 条目很重要,并且在 JVMS 中没有为这种类型指定格式是一个遗漏。

结果

以上仅描述和解释了现状,表明没有规范告诉编译器的行为与当前状态不同。显然,这不是一个完全可取的情况。

  1. 有人可能想联系 Oracle,提到 Java 8 引入了部分 JVMS 未涵盖的情况。一旦局部变量也受到类型推断的影响,这种情况可能会变得更加相关
  2. 任何观察当前情况负面影响的人都被邀请加入rfe 494198 (ecj),否则优先级较低。

更新: 同时有人报告了一个示例,其中需要常规签名属性(不能机会主义地省略)来编码无法根据 JVMS 编码的类型。在这种情况下,javac 也会创建未指定的字节码。根据后续行动, 任何变量都不应该有这样的类型,但我认为这个讨论还没有结束(诚然 JLS 还不能确保这个目标)。

更新 2: 在收到规范作者的建议后,我看到了最终解决方案的三个部分:

(1) 任何字节码属性中的每个类型签名都必须遵守 JVMS 4.7.9.1 中的语法。ecj!和 javac都不<captured wildcard>合法。

(2) 编译器应该在不存在合法编码的情况下近似类型签名,例如,通过使用擦除而不是捕获。对于 LVTT 条目,这种近似应该被认为是合法的。

(3) JLS必须确保只有可使用 JVMS 4.7.9.1 编码的类型出现在必须生成签名属性的位置。

对于 ecj 的未来版本,项目 (1) 和 (2) 已解决。当 javac 和 JLS 将相应地修复时,我不能谈论时间表。

于 2016-05-19T21:52:33.127 回答