【问题标题】:Lambda field capturing local variable .isSynthetic() returns false捕获局部变量 .isSynthetic() 的 Lambda 字段返回 false
【发布时间】:2020-04-14 04:28:08
【问题描述】:

在回答this question 关于捕获局部变量的 lambda 时,我定义了一个简单的 lambda 来捕获一个局部变量,并表明 lambda 有一个包含该变量值的字段。根据各种来源(例如herehere),当 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 方法文档,规范和文档是否使用“合成”一词来表示不同的东西,或者这是一个错误?

【问题讨论】:

  • 鉴于整个类都是合成的,也许他们认为没有必要将每个字段也标记为合成的。
  • @RealSkeptic 很有趣,也许就是这样,但 JVMS 似乎并没有为此留下回旋余地:它说 “必须标记” 合成的。跨度>
  • 它提供了两种将其标记为合成的方法。请注意 isSynthetic 返回的定义说它应该是合成的“根据 JLS”,而 JLS 正在谈论“合成构造”并且对此类构造的各种成员含糊其辞。
  • 由于 JLS 只允许“构造”是合成的,而 isSynthetic 文档说它根据 JLS 确定该字段是否是合成的,我认为这意味着该字段本身算作一个构造,而不仅仅是构造的成员。但我认为你可能会有所收获。
  • JLS 的其他地方暗示变量应该算作构造,例如“枚举常量的类体内的任何构造的明确赋值/取消赋值状态受类的常用规则控制。”

标签: java lambda closures language-lawyer java-10


【解决方案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 表达式,整个类已被标记为 synthetic,但没有任何成员。

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

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

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

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

正如John Rose 所说:

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

你不能依赖任何你认为的意思, 即使它看起来有一个类文件结构。

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

返回:

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

这将我们带到JLS §13.1

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

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

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

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

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

  • 其中 lambda 表达式的类型为 U,对于 U 的每个非 static 成员方法 m

    如果U 的函数类型具有m 签名的子签名,则该类声明一个覆盖m 的方法。该方法的主体具有评估 lambda 主体(如果它是一个表达式)或执行 lambda 主体(如果它是一个块)的效果;如果预期结果,则从方法返回。

    如果被覆盖的方法类型的擦除在其签名上与U 函数类型的擦除不同,则在评估或执行 lambda 主体之前,方法的主体会检查每个参数值是否是一个实例擦除U函数类型中对应参数类型的子类或子接口;如果没有,则抛出 ClassCastException

  • 该类不覆盖目标函数接口类型或上述其他接口类型的其他方法,尽管它可能会覆盖Object 类的方法。

所以规范没有涵盖实际类的许多属性,这是故意的。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    • 2020-02-13
    • 2017-08-14
    • 1970-01-01
    相关资源
    最近更新 更多