Lambda 字段捕获局部变量 .isSynthetic() returns false

Lambda field capturing local variable .isSynthetic() returns false

在回答 this question about lambdas which capture local variables, I defined a simple lambda which captures a local variable, and showed that the lambda has a field with that variable's value. According to various sources (e.g. here, here), when a lambda captures a local variable, its value is stored in a "synthetic" field. This seems to be implied by the Java Virtual Machine Specification (§4.7.8) 时说:

A class member that does not appear in the source code must be marked using a Synthetic attribute, or else it must have its ACC_SYNTHETIC flag set. The only exceptions to this requirement are compiler-generated methods which are not considered implementation artifacts, namely the instance initialization method representing a default constructor of the Java programming language (§2.9.1), the class or interface initialization method (§2.9.2), and the Enum.values() and Enum.valueOf() methods.

lambda的字段不是定义的异常之一,并且lambda的字段在源代码中没有声明,所以根据我的理解应该是根据这个规则合成的。

场的存在可以很容易地通过反射证明。但是,当我使用 Field.isSynthetic 方法检查时,它实际上是 returns false。这个方法的文档说:

Returns true if this field is a synthetic field; returns false otherwise.

我正在 Java 10.0.1:

中使用 JShell 进行测试
> class A { static Runnable a(int x) { return () -> System.out.println(x); } }
|  created class A

> Runnable r = A.a(5);
r ==> A$$Lambda/1413653265@548e7350

> import java.lang.reflect.Field;

> Field[] fields = r.getClass().getDeclaredFields();
fields ==> Field[1] { private final int A$$Lambda/1413653265.arg }

> fields[0].isSynthetic()
 ==> false

同样的行为发生在 JShell 之外:

import java.lang.reflect.Field;

public class LambdaTest {
    static Runnable a(int x) {
        return () -> System.out.println(x);
    }

    public static void main(String[] args) {
        Runnable r = a(5);
        Field[] fields = r.getClass().getDeclaredFields();
        boolean isSynthetic = fields[0].isSynthetic();
        System.out.println("isSynthetic == " + isSynthetic); // false
    }
}

这种差异的解释是什么?我是否误解了 JVMS,我是否误解了 Field.isSynthetic 方法文档,规范和文档是否使用 "synthetic" 一词表示不同的意思,或者这是一个错误?

一般来说,您对为捕获变量生成的字段的合成 性质的理解是正确的。

当我们使用下面的程序时

public class CheckSynthetic {
    public static void main(String[] args) {
        new CheckSynthetic().check(true);
    }
    private void check(boolean b) {
        print(getClass());
        print(new Runnable() { public void run() { check(!b); } }.getClass());
        print(((Runnable)() -> check(!b)).getClass());
    }
    private void print(Class<?> c) {
        System.out.println(c.getName()+", synthetic: "+c.isSynthetic());
        Stream.of(c.getDeclaredFields(),c.getDeclaredConstructors(),c.getDeclaredMethods())
            .flatMap(Arrays::stream)
            .forEach(m->System.out.println("\t"+m.getClass().getSimpleName()+' '+m.getName()
                                           +", synthetic: "+m.isSynthetic()));
    }
}

我们得到类似

的东西
CheckSynthetic, synthetic: false
    Constructor CheckSynthetic, synthetic: false
    Method main, synthetic: false
    Method check, synthetic: false
    Method print, synthetic: false
    Method lambda$print, synthetic: true
    Method lambda$check[=11=], synthetic: true
CheckSynthetic, synthetic: false
    Field val$b, synthetic: true
    Field this[=11=], synthetic: true
    Constructor CheckSynthetic, synthetic: false
    Method run, synthetic: false
CheckSynthetic$$Lambda/0x0000000840074440, synthetic: true
    Field arg, synthetic: false
    Field arg, synthetic: false
    Constructor CheckSynthetic$$Lambda/0x0000000840074440, synthetic: false
    Method run, synthetic: false
    Method get$Lambda, synthetic: false

在 JDK-11 之前,您还会找到类似

的条目
    Method access[=12=]0, synthetic: true

在外classCheckSynthetic.

因此对于匿名内部 class,字段 this[=14=]val$b 被标记为 synthetic,正如预期的那样。

对于 lambda 表达式,整个 class 已被标记为 synthetic,但其成员的 none。

一种解释可能是将 class 标记为合成在这里已经足够了。考虑到 JVMS §4.7.8:

A class member that does not appear in the source code must be marked using a Synthetic attribute, or else it must have its ACC_SYNTHETIC flag set.

我们可以说,当 class 没有出现在源代码中时,就没有可以检查成员声明存在的源代码。

但更重要的是该规范适用于 class 文件,而我们这些对更多细节感兴趣的人知道在幕后,LambdaMetafactory 的参考实现将在class 文件格式以创建匿名 class,这是一个未指定的实现细节。

正如John Rose所说:

VM anonymous classes are an implementation detail that is opaque to system components except for the lowest layers of the JDK runtime and the JVM itself. […] Ideally we should not make them visible at all, but sometimes it helps (e.g., with single stepping through BCs).

You can't rely on any of this meaning what you think it means, even if it appears to have a classfile structure.

所以我们不应该对这个 class 文件结构进行推理,而只关注可见的行为,即 Field.isSynthetic() 的 return 值。虽然可以合理地假设在幕后,这个实现只会报告字节码是否有标志或属性,但我们必须关注与字节码无关的 contract of isSynthetic:

Returns:

true if and only if this field is a synthetic field as defined by the Java Language Specification.

这将我们带到 JLS §13.1:

  1. A construct emitted by a Java compiler must be marked as synthetic if it does not correspond to a construct declared explicitly or implicitly in source code, unless the emitted construct is a class initialization method (JVMS §2.9).

不仅构造“在源代码中隐式声明”的可能性非常模糊,标记为合成的要求仅限于“Java 编译器发出的构造”。但是在运行时为 lambda 表达式生成的 classes 不是由 Java 编译器生成的,它们是由字节码工厂自动生成的。这不仅仅是狡辩,因为整个 §13 都是关于 二进制兼容性 ,但是在单个运行时内生成的短暂 classes 根本不受二进制兼容性的约束,因为当前运行时是唯一必须处理它们的软件。

对运行时的要求 class 在 JLS §15.27.4 中指定:

The value of a lambda expression is a reference to an instance of a class with the following properties:

  • The class implements the targeted functional interface type and, if the target type is an intersection type, every other interface type mentioned in the intersection.

  • Where the lambda expression has type U, for each non-static member method m of U:

    If the function type of U has a subsignature of the signature of m, then the class declares a method that overrides m. The method's body has the effect of evaluating the lambda body, if it is an expression, or of executing the lambda body, if it is a block; if a result is expected, it is returned from the method.

    If the erasure of the type of a method being overridden differs in its signature from the erasure of the function type of U, then before evaluating or executing the lambda body, the method's body checks that each argument value is an instance of a subclass or subinterface of the erasure of the corresponding parameter type in the function type of U; if not, a ClassCastException is thrown.

  • The class overrides no other methods of the targeted functional interface type or other interface types mentioned above, although it may override methods of the Object class.

因此规范并未涵盖实际 class 的许多属性,这是有意为之。

所以当Field.isSynthetic()的结果仅由Java语言规范确定,但检查字段的class不符合规范时,结果未指定。

现在我们可以观察生成的 class 的某些人工制品,这些人工制品是否应该遵循与普通 classes 的相似性的某些预期,但没有足够的信息讨论那个。最值得注意的是,在任何引用的规范中都没有一个词说明为什么我们必须将构造标记为合成的,以及标记的存在或不存在会产生什么后果。

实际测试显示 Java 编译器,即 javac,在尝试在源代码级别访问合成成员时将它们视为 nonexistent,但这在任何地方都没有指定。此外,此行为与运行时生成的 class 无关,Java 编译器从未见过。相比之下,对于通过反射访问,合成标志似乎根本没有作用。