使用 Eclipse 编译器编译时 LocalVariableTypeTable 中出现奇怪的“!*”条目

Strange "!*" entry in LocalVariableTypeTable when compiling with Eclipse compiler

让我们使用 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检查生成的class文件。最有趣的是为 (a, t) -> {} lambda 生成的合成方法:

  private static void lambda(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 规范 covers LocalVariableTypeTable 属性并表示:

The constant_pool entry at that index must contain a CONSTANT_Utf8_info structure (§4.4.7) representing a field signature which encodes the type of a local variable in the source program (§4.7.9.1).

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

的任何内容

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

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

在内部,ecj 使用两种风格的 CaptureBinding,一种用于实现 JLS 18.4 calls "fresh type variables", the other to implement captures a la JLS 5.1.10(使用与 "free type variables" 相同的行话)。两者都使用 ! 生成签名。仔细看看,在这个例子中我们有一个 "old-style" 捕获:t 有类型 capture#1-of ?,捕获 Stream<T>.

中的 <T>

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

我无法让 javac 为 lambda 发出任何 LocalVariableTypeTable,所以他们可能只是避免回答这个问题。

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

This difference is only significant for variables whose type uses a type variable or parameterized type.

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

后果

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

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

更新: 与此同时,有人报告了一个 example where a regular Signature attribute (which cannot be opportunistically omitted) is required to encode a type which cannot be encoded according to JVMS. In that case also javac creates unspecified byte code. According to a follow-up no variable should ever have such a type,但我认为这个讨论还没有结束(诚然,JLS 还没有确保这个目标)。

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

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

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

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

未来版本的 ecj 项目 (1) 和 (2) 已经 resolved。我不能谈论 javac 和 JLS 何时会相应修复的时间表。