Java记录:近距离观察

记录是Java 14中的一项新预览功能,它提供了一种很好的紧凑语法来声明应该是哑数据持有人的类。在本文中,我们将了解Records的外观。系好安全带!

类表示

让我们从一个非常简单的示例开始:

如何使用以下代码编译此代码javac

然后,可以使用来窥视生成的字节码javap

这将打印以下内容:

Java记录:近距离观察_第1张图片

有趣的是,类似于Enums,Records是具有一些基本属性的普通Java类

  • 它们被声明为final类,因此我们不能从它们继承。
  • 它们已经从另一个名为的类继承java.lang.Record。因此,Records不能扩展任何其他类,因为Java不允许多重继承。
  • 记录可以实现其他接口。
  • 对于每个组件,都有一个访问器方法,例如maxmin
  • 有自动生成的实现toStringequals并且hashCode基于所有组件。
  • 最后,有一个自动生成的构造函数,它接受所有组件作为其参数。

而且,java.lang.Record仅仅是一个受保护的无参数构造函数和一些其他基本抽象方法的抽象类:

Java记录:近距离观察_第2张图片

数据类的奇怪案例

来自Kotlin或Scala的背景,可能会发现Java中的Records,Kotlin中的 Data类和Scala中的 Case类之间的一些相似之处。从表面上看,它们都有一个非常基本的目标:促进编写数据持有人。

尽管具有基本的相似性,但字节码级别的情况却大不相同。

Kotlin的资料类别

为了进行比较,让我们看一下Kotlin数据类,其等效于Range

与Records相似,Kotlin编译器基于这种简单的一线式生成访问器方法,default toStringequalshashCode实现以及其他一些功能。

让我们看看Kotlin编译器如何为以下代码生成代码toString

Java记录:近距离观察_第3张图片

我们发出javap -c -v Range来生成此输出。另外,为了简洁起见,这里我们使用简单的类名。

无论如何,Kotlin都使用StringBuilder来生成字符串表示形式,而不是多个字符串串联(就像任何体面的Java开发人员一样!)。那是:

  • 首先,它创建一个新实例StringBuilder(索引0、3、4)。
  • 然后,它附加文字Range(min=字符串(索引7、9)。
  • 然后附加实际的最小值(索引12、13、16)。
  • 然后,它附加文字, max=(索引19、21)。
  • 然后附加实际的最大值(索引24、25、28)。
  • 然后,通过附加)文字(索引31、33)来关闭括号。
  • 最后,它构建StringBuilder实例并返回它(索引36、39)。

基本上,数据类中的属性越多,字节码越长,因此启动时间也越长。

Scala案例类

让我们case class用Scala 编写等效的代码:

乍一看,Scala似乎生成了一个简单得多的toString实现:

Java记录:近距离观察_第4张图片

但是,toString调用scala.runtime.ScalaRunTime._toString static方法。依次调用该productIterator方法来迭代此产品类型。该迭代器调用productElement方法,如下所示:

Java记录:近距离观察_第5张图片

基本上可以切换的所有属性case class。例如,如果productIterator想要第一个属性,则返回min。同样,当需要productIterator第二个元素时,它将返回该max值。否则,它将抛出一个实例,IndexOutOfBoundsException以发出超出范围的请求。

同样,我们在中拥有的属性越多case class,我们将拥有更多的切换臂。因此,字节码的长度与属性的数量成正比。换句话说,与Kotlin的问题相同data class

动态调用

让我们仔细看看为Java记录生成的字节码:

Java记录:近距离观察_第6张图片

无论记录分量的数量如何,这都是字节码。简单,精致,优雅的解决方案。但是这invokedynamic东西如何工作?

印地简介

Invoke Dynamic(也称为Indy)是JSR 292的一部分,旨在增强JVM对动态语言的支持。在Java 7中首次发布之后,该invokedynamic操作码及其java.lang.invoke行李箱被基于动态JVM的语言(如JRuby)广泛使用。

尽管Indy是专门为增强动态语言支持而设计的,但它提供的功能远不止这些。事实上,适合语言设计者需要从动态类型的杂技到动态策略的任何形式的动态的地方使用!例如,即使Java是静态类型的语言,Java 8 Lambda表达式实际上是使用实现的invokedynamic

用户定义的字节码

一段时间以来,JVM确实支持四种方法调用类型:invokestatic调用静态方法,invokeinterface调用接口方法,invokespecial调用构造函数super()private方法以及invokevirtual调用实例方法。

尽管它们有所不同,但是这些调用类型具有一个共同的特征:我们无法用自己的逻辑丰富它们。相反,invokedynamic使我们能够以所需的任何方式引导调用过程。然后,JVM负责直接调用bootstrapped方法。

Indy如何工作?

JVM第一次看到invokedynamic指令时,会调用一个称为Bootstrap Method的特殊静态方法。bootstrap方法是一段我们编写的Java代码,用于准备实际的调用 逻辑

Java记录:近距离观察_第7张图片

然后bootstrap方法返回的实例java.invoke.CallSite。这CallSite引用了实际方法,即MethodHandle从现在开始,每次JVM invokedynamic再次看到该指令时,它都会跳过Slow Path并直接调用基础可执行文件。除非有所更改,否则JVM会继续跳过慢速路径。

为什么选择印地?

与反射API相对,该java.lang.invokeAPI非常有效,因为JVM可以完全查看所有调用。因此,只要我们尽可能避免缓慢的路径,JVM可能会应用各种优化!

除了效率方面的论点之外,由于其简单性,该invokedynamic方法更可靠,更不易碎。

此外,为Java记录生成的字节码与属性的数量无关。因此,更少的字节码和更快的启动时间。

最后,我们假设Java的新版本包括一个新的且更有效的bootstrap方法实现。使用invokedynamic,我们的应用无需重新编译即可利用此改进。这样,我们就具有某种正向二进制兼容性。另外,这就是我们正在讨论的动态策略 

对象方法

既然我们已经对Indy足够熟悉,那么让我们了解一下invokedynamicRecords字节码:

看看我在Bootstrap方法表中找到的内容:

Java记录:近距离观察_第8张图片

因此,将调用Records 的引导方法,该方法bootstrap位于java.lang.runtime.ObjectMethods类中。如您所见,此引导程序方法需要以下参数:

  • MethodHandles.Lookup表示查找上下文的实例(该Ljava/lang/invoke/MethodHandles$Lookup部分)。
  • 方法名(即toStringequalshashCode等等)引导将要链接。例如,当值为时toString,bootstrap将返回一个ConstantCallSiteCallSite永远不变)的指针,该指针指向toString该特定Record 的实际实现。
  • 所述TypeDescriptor的方法(Ljava/lang/invoke/TypeDescriptor部分)。
  • 类型标记,即Class,代表Record类的类型。这是Class在这种情况下。
  • 所有组件名称的分号分隔列表,即min;max
  • MethodHandle每个组件一个。这样,bootstrap方法就可以MethodHandle基于该特定方法实现的组件创建一个。

invokedynamic指令将所有这些参数传递给bootstrap方法。Bootstrap方法又返回的实例ConstantCallSite。这ConstantCallSite持有对请求的方法实现的引用,例如toString

反思记录

对该java.lang.ClassAPI进行了改进以支持Records。例如,给定a Class,我们可以使用new isRecord方法检查它是否为Record :

Java记录:近距离观察_第9张图片

它显然会返回false非记录类型:

还有一种getRecordComponents方法,该方法RecordComponent以与原始记录中定义的顺序相同的顺序返回数组。每个java.lang.reflect.RecordComponent代表一个记录组件或当前记录类型的变量。例如,RecordComponent.getName返回组件名称:

Java记录:近距离观察_第10张图片

以相同的方式,该getType方法为每个组件返回类型令牌:

Java记录:近距离观察_第11张图片

甚至可以通过getAccessor以下方式获取访问器方法的句柄:

Java记录:近距离观察_第12张图片

注释记录

Java允许您注释记录,只要注释适用于记录或其成员即可。此外,还会有一个ElementType名为的新注释RECORD_COMPONENT。此目标的注释只能在记录组件上使用:

序列化

任何与序列化无关的新Java功能都是不完整的。但这一次,这种关系听起来并不像我们过去那样令人讨厌。

尽管记录默认情况下不是可序列化的,但是可以通过实现java.io.Serializable标记接口来使它们成为可能。

可序列化记录的序列化和反序列化与普通可序列化对象不同。更新后的javadoc的ObjectInputStream规定:

  • 记录对象的序列化形式是从记录组件派生的一系列值。
  • 记录对象序列化或外部化的过程无法定制 ; 任何类特异性writeObjectreadObjectreadObjectNoDatawriteExternal,和readExternal由记录类定义的方法将被忽略的序列化和反序列化过程。
  • serialVersionUID记录的类0L,除非明确声明。

结论

Java Records将提供一种封装数据持有者的新方法。即使目前它们在功能方面受到限制(与Kotlin或Scala提供的功能相比),实现也很可靠。

Records的第一个预览版将于2020年3月发布。在本文中,我们使用了该openjdk 14-ea 2020-03-17版本,因为Java 14尚未发布!

有什么问题可以加下qq:2062583349。也可添加vx:admindesire,有java、python、web等习资料和视频课程干货”。欢迎交流!

 

你可能感兴趣的:(java)