java记录紧凑构造函数字节码

Eug*_*ene 2 java bytecode record java-17

我正在将一个简单的类转换为一条记录,如下所示:

public class Mine {
    private final String name;
    public Mine(String name) {
        this.name = name == null ? "a" : name;
    }
}
Run Code Online (Sandbox Code Playgroud)

本能地,我是这样写的:

public record Mine(String name) {
    public Mine {
        this.name = name == null ? "a" : name;
    }
}
Run Code Online (Sandbox Code Playgroud)

无法编译:cannot assign a value to final variable name.

我有点困惑,因为这个紧凑的构造函数有效:

    public Mine {
        name = name == null ? "a" : name;
    }
Run Code Online (Sandbox Code Playgroud)

我无法真正理解发生了什么,所以我决定查看字节码:

         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: aload_1
         5: ifnonnull     13
         8: ldc           #7                  // String a
        10: goto          14
        13: aload_1
        14: astore_1
        15: aload_0
        16: aload_1
        17: putfield      #9                  // Field name:Ljava/lang/String;
        20: return

Run Code Online (Sandbox Code Playgroud)

看起来javac,当它看到对记录变量()的赋值时,name实际上会将其“保存”到局部变量:

astore_1
Run Code Online (Sandbox Code Playgroud)

然后它就这样做了this.name=<local>,像这样:

public Mine {
   String local = name == null ? "a" : name;
   this.name = local;
}
Run Code Online (Sandbox Code Playgroud)

如果您查看等效类的字节码:

public class Mine {
    private final String name;

    public Mine(String name) {
        String local = name == null ? "a" : name;
        this.name = local;
    }
}
Run Code Online (Sandbox Code Playgroud)

它几乎是相同的,区别aload_2在于使用 代替aload_1,这不是什么大问题,并且很可能与兼容性原因有关。

有人可以确认我的理解是否正确吗?

rzw*_*oot 11

  • 紧凑的记录构造函数具有与每个记录组件相对应的隐式参数,与记录的声明相匹配。(你写public Mine {},它的行为就像public Mine(String name) {},因为你的记录被定义为record Mine(String name).
  • 这些是参数,当您在 Mine 构造函数中引用时name,它指的是该参数。它们不是最终的。
  • 在构造函数的最后,所有参数(即它们当时所具有的值;您可以更改它们,因为它们不是最终的)都被写入字段中,这些字段是且final不能成为非最终的。就好像编译器this.name = name;在末尾和每个return;. 您不能要求编译器跳过此步骤。
  • 鉴于您的字段是在最后自动分配的,并且始终是final,您不能在实际代码中的任何地方分配它们。毕竟,如果你这样做,那么你就分配了它们,后来自动生成的this.name = name分配了它们,这是非法的 java.lang. 因此,this.anyField =是记录构造函数中的即时错误,你永远不能写这个。
  • 鉴于字段是在最后分配的,在此之前读取它们是无效的,因为它们尚未设置。因此,就像前面的项目符号所说的那样,分配它们(this.field =...)必然是一个错误,读取它们也是如此。结论:任何记录构造函数this.field 都是错误的。当你编写它时,编译器知道你的意思(即语法上它是有效的java;编译器理解它的意思),但当你这样做时总是会发出错误(即它在语义上无效)。
  • 因此,生活很简单:只需使用“字段名称”(此处为name),不使用this,一切就正常了。事实上,鉴于它是一个隐藏参数,您甚至无法将其隐藏:String name = "haha shadowed out!";在记录构造函数内也不合法,出于同样的原因也void testMethod(String x) { String x = ""; }无法编译。您不能在同一范围内使用相同的名称重新声明变量。

您会看到它“具有隐藏参数并将它们写入字节码末尾的字段”。具体来说,最后一部分:

15: aload_0
16: aload_1
17: putfield      #9   
Run Code Online (Sandbox Code Playgroud)

是 的字节码this.name = param1。槽 0 通常始终用于 this 引用,槽 1 在此用于该参数。操作是“将此值写入该字段”(这就是所做的putfield),为了完成这项工作,堆栈需要:[A]接收器,以及[B]要放在那里的值。因此,aload_0(loads this) 然后aload_1(loads nameparam)。

name = name == null ? "a" : name会覆盖上面的字节码最终通过aload_1这部分加载的内容:

4: aload_1
5: ifnonnull     13
8: ldc           #7                  // String a
10: goto          14
13: aload_1
14: astore_1
Run Code Online (Sandbox Code Playgroud)

aload_1仍然是'load param name',所以,4加载它,5对其进行空检查并消耗它(字节码是基于堆栈的,因此aload_1将值推入堆栈。name如果ifnonnullvalue 不为 null,如果为 null,则直接转到指令。

如果它为空,则下一条指令ldc(这是“加载常量”的缩写),这会将常量值推送到"a"堆栈上。然后转到 14。

如果它不为空,我们跳到 13:我们aload_1再次(我们将参数值压name入堆栈),最终也到达 14,它现在存储 this ( astore_1)。

旁注:从字节码角度来看,这似乎非常低效(为什么aload_1然后astore_1?为什么不使用跳转语句跳过 aload 和 astore ?) - 但字节码并不是设计为高效发出的。与eg 不同,C 编译器javac故意不进行优化,它没有优化层(没有-O3或类似eg 那样的命令行切换方式gcc),并且必须严格遵循规范。

原因是:这种优化由java完成的,但是是在运行时由热点完成的。不是由javac.

  • @尤金如果您熟悉所有这些,那么显然您唯一实际问题的答案是“是”。 (3认同)
  • 如果你只是想咆哮,你应该在你的“问题”中提到这一点。或者可能首先跳过SO。SO 问题和答案旨在供更广泛的消费:是的,您问过它,但 SO 的要点是其他人可能在某些时候也会问它。仅仅因为这都是_you_踩过的地方并不重要。唯一相关的部分是:它是否回答了问题。我将问题解释为“请解释在编写修改记录组件的紧凑构造函数时生成的字节码”。我认为这彻底回答了这个问题。 (2认同)
  • @尤金但是为什么呢?如前所述,如果“name”只是字段,那也没关系。所以你不必记住“name”是参数。相比之下,你为什么首先尝试写“this.name”?只有当您已经知道“name”是参数时,这才有意义。 (2认同)