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

2022-09-04 22:42:58

让我们使用 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

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

  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   !*

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

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

§4.7.9.1 定义了字段签名的语法,如果我理解正确,它不包括与 .!*

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


答案 1

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

在内部,ecj使用两种风格,一种用于实现JLS 18.4所谓的“新鲜类型变量”,另一种用于实现捕获la JLS 5.1.10(它使用相同的“自由类型变量”术语)。两者都使用 生成签名。仔细看一下,在这个例子中,我们有一个“旧式”捕获:has type ,捕获in。CaptureBinding!tcapture#1-of ?<T>Stream<T>

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

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

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

仅当其类型使用类型变量或参数化类型的变量时,此差异才显著。

根据 JLS,捕获是新类型变量,因此 LVTT 条目很重要,并且在 JVMS 中省略不指定此类型的格式。

后果

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

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

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

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

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

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

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

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


答案 2

推荐