捕获局部变量 .isSynthetic() 的 Lambda 字段返回 false

Lambda field capturing local variable .isSynthetic() returns false

提问人:kaya3 提问时间:12/23/2019 最后编辑:kaya3 更新时间:1/22/2020 访问量:522

问:

在回答这个关于捕获局部变量的 lambda 的问题时,我定义了一个简单的 lambda,它捕获了一个局部变量,并表明 lambda 有一个包含该变量值的字段。根据各种来源(例如,这里),当 lambda 捕获局部变量时,其值存储在“合成”字段中。Java 虚拟机规范 (§4.7.8) 似乎暗示了这一点,它说:

未出现在源代码中的类成员必须使用 Synthetic 属性进行标记,否则必须设置其ACC_SYNTHETIC标志。此要求的唯一例外是编译器生成的方法,这些方法不被视为实现工件,即表示 Java 编程语言的默认构造函数的实例初始化方法 (§2.9.1)、类或接口初始化方法 (§2.9.2) 以及 Enum.values() 和 Enum.valueOf() 方法。

lambda 的字段不是定义的异常之一,并且 lambda 的字段未在源代码中声明,因此根据我的理解,该字段应该根据此规则进行合成。

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

如果此字段是合成字段,则返回 true;否则返回 false。

我在 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$15/1413653265@548e7350

> import java.lang.reflect.Field;

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

> fields[0].isSynthetic()
$5 ==> 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

Java Lambda 闭包语言 律师 java-10

评论

3赞 RealSkeptic 12/23/2019
鉴于整个类都是合成的,也许他们认为没有必要将每个字段也标记为合成。
0赞 kaya3 12/23/2019
@RealSkeptic 有趣的是,也许就是这样,但 JVMS 似乎没有为此留下回旋余地:它说“必须标记”合成。
1赞 RealSkeptic 12/23/2019
它提供了两种将其标记为合成的方法。请注意,返回的定义说它应该是“根据 JLS”合成的,而 JLS 谈论的是“合成结构”,并且对此类结构的各种成员含糊不清。isSynthetic
0赞 kaya3 12/23/2019
由于 JLS 只允许“构造”是合成的,并且文档说它根据 JLS 确定字段是否是合成的,因此我认为这意味着字段本身算作一个构造,而不仅仅是构造的成员。但我认为你可能在做些什么。isSynthetic
0赞 kaya3 12/23/2019
JLS 的其他地方暗示变量应该算作结构,例如“枚举常量的类体中任何结构的确定赋值/取消赋值状态都受类的通常规则的约束。

答:

7赞 Holger 1/22/2020 #1

通常,您对为捕获的变量生成的字段的合成性质的理解是正确的。

当我们使用以下程序时

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$1, synthetic: true
    Method lambda$check$0, synthetic: true
CheckSynthetic$1, synthetic: false
    Field val$b, synthetic: true
    Field this$0, synthetic: true
    Constructor CheckSynthetic$1, synthetic: false
    Method run, synthetic: false
CheckSynthetic$$Lambda$21/0x0000000840074440, synthetic: true
    Field arg$1, synthetic: false
    Field arg$2, synthetic: false
    Constructor CheckSynthetic$$Lambda$21/0x0000000840074440, synthetic: false
    Method run, synthetic: false
    Method get$Lambda, synthetic: false

在 JDK-11 之前,您还会发现一个类似

    Method access$000, synthetic: true

在外部类中.CheckSynthetic

因此,对于匿名内部类,字段 和 被标记为合成,正如预期的那样。this$0val$b

对于 lambda 表达式,整个类都被标记为合成类,但其成员均未被标记为合成类。

一种解释可能是,在这里将类标记为合成类就足够了。考虑 JVMS §4.7.8

源代码中未出现的类成员必须使用属性进行标记,否则必须设置其标志。SyntheticACC_SYNTHETIC

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

但更重要的是,此规范适用于类文件,虽然我们这些对更多细节感兴趣的人知道,在后台,LambdaMetafactory 的参考实现将生成类文件格式的字节码以创建匿名类,但这是一个未指定的实现细节。

正如约翰·罗斯(John Rose)所说:

VM 匿名类是一个实现细节,除了 JDK 运行时的最低层和 JVM 本身之外,它对系统组件是不透明的。[...]理想情况下,我们不应该让它们完全可见,但有时它会有所帮助(例如,单步通过 BC)。

...

你不能依赖任何你认为它意味着什么的意义, 即使它看起来具有类文件结构。

因此,我们不应该对这个类文件结构进行推理,而只关注可见的行为,即 的返回值。虽然可以合理地假设,在后台,这个实现只会报告字节码是否具有标志或属性,但我们必须关注 isSynthetic字节码独立合约:Field.isSynthetic()

返回:

当且仅当此字段是 Java 语言规范定义的合成字段时,true 为真。

这就把我们带到了 JLS §13.1

  1. 如果 Java 编译器发出的构造与源代码中显式或隐式声明的构造不对应,则必须将其标记为合成构造,除非发出的构造是类初始化方法 (JVMS §2.9)。

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

JLS §15.27.4 中指定了对运行时类的要求:

lambda 表达式的值是对具有以下属性的类实例的引用:

  • 该类实现目标函数接口类型,如果目标类型是交集类型,则实现交集中提到的所有其他接口类型。

  • 其中 lambda 表达式的类型为 ,对于以下每个非成员方法:UstaticmU

    如果函数类型具有 的子签名,则该类声明一个重写 的方法。如果该方法的主体是计算 lambda 主体(如果它是一个表达式),或者执行 lambda 主体(如果它是一个块);如果结果是预期的,则从方法返回该结果。Umm

    如果被重写的方法类型的擦除与其函数类型的擦除不同,则在计算或执行 lambda 主体之前,方法的主体会检查每个参数值是否是函数类型 中相应参数类型的擦除的子类或子接口的实例;如果没有,则抛出 A。UUClassCastException

  • 该类不会重写目标函数接口类型的其他方法或上面提到的其他接口类型,尽管它可以重写该类的方法。Object

因此,该规范没有涵盖实际类的许多属性,这是有意为之的。

因此,当结果仅由 Java 语言规范确定,但检查字段的类不在规范时,结果是未指定的。Field.isSynthetic()

既然我们可以观察到生成类的某些工件,那么这些工件是否应该遵循与普通类相似性的某些期望,这还有解释的余地,但没有足够的信息来讨论这一点。最值得注意的是,在任何引用的规范中,没有一个词说明为什么我们必须将结构标记为合成结构,以及标记的存在与否会产生什么后果。

实际测试表明,Java 编译器(即 )在尝试在源代码级别访问合成成员时将其视为不存在,但尚未在任何地方指定。此外,此行为与 Java 编译器从未见过的运行时生成的类无关。相比之下,对于通过 Reflection 进行的访问,合成标志似乎根本没有效果。javac