让我们使用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) -> {}
λ生成的合成方法:
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
属性中是否存在其他类似的字符串?
ecj使用令牌!
对泛型签名中的捕获类型进行编码。因此,!*
表示对无边界通配符的捕获。
在内部,ecj使用两种类型的CaptureBinding
,一种用于实现JLS 18.4所称的"新鲜类型变量",另一种用于捕获la JLS 5.1.10(使用相同的"自由类型变量"行话)。两者都使用!
生成签名。仔细看,在本例中,我们有一个"旧式"捕获:t
的类型为capture#1-of ?
,捕获Stream<T>
中的<T>
。
问题是:JVMS 4.7.9.1。似乎没有为此类新类型变量定义编码(在其他属性中,源代码中没有对应关系,因此没有名称)。
我无法让javac
为lambda发出任何LocalVariableTypeTable
,所以他们可能只是避免回答这个问题。
既然两个编译器都同意将t
推断为捕获,为什么一个编译器生成LVTT,而另一个编译器不生成呢?JVMS 4.7.14具有此
这种差异仅对其类型使用类型变量或参数化类型的变量有效。
根据JLS的说法,捕获是新的类型变量,因此LVTT条目是重要的,并且在JVMS中没有为该类型指定格式是一种遗漏。
后果
以上仅描述和解释了现状,表明没有规范告诉编译器的行为与当前状态不同。显然,这不是一个完全可取的情况。
- 有人可能想联系Oracle,提到Java8引入了JVMS部分未涵盖的情况。一旦局部变量也受到类型推断的影响,这种情况可能会变得更加相关
- 任何观察到当前局势负面影响的人都被邀请加入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可编码的类型才会出现在必须生成Signature属性的位置。
对于ecj的未来版本,项目(1)和(2)已得到解决。我不能谈论javac和JLS将被相应地修复的时间表。