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).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.