记录是Java 14中的一项新预览功能,它提供了一种很好的紧凑语法来声明应该是哑数据持有人的类。在本文中,我们将了解Records的外观。系好安全带!
让我们从一个非常简单的示例开始:
如何使用以下代码编译此代码javac
:
然后,可以使用来窥视生成的字节码javap
:
这将打印以下内容:
有趣的是,类似于Enums,Records是具有一些基本属性的普通Java类:
final
类,因此我们不能从它们继承。java.lang.Record
。因此,Records不能扩展任何其他类,因为Java不允许多重继承。max
和min
。toString
,equals
并且hashCode
基于所有组件。而且,java.lang.Record
仅仅是一个受保护的无参数构造函数和一些其他基本抽象方法的抽象类:
来自Kotlin或Scala的背景,可能会发现Java中的Records,Kotlin中的 Data类和Scala中的 Case类之间的一些相似之处。从表面上看,它们都有一个非常基本的目标:促进编写数据持有人。
尽管具有基本的相似性,但字节码级别的情况却大不相同。
为了进行比较,让我们看一下Kotlin数据类,其等效于Range
:
与Records相似,Kotlin编译器基于这种简单的一线式生成访问器方法,default toString
,equals
和hashCode
实现以及其他一些功能。
让我们看看Kotlin编译器如何为以下代码生成代码toString
:
我们发出javap -c -v Range
来生成此输出。另外,为了简洁起见,这里我们使用简单的类名。
无论如何,Kotlin都使用StringBuilder
来生成字符串表示形式,而不是多个字符串串联(就像任何体面的Java开发人员一样!)。那是:
StringBuilder
(索引0、3、4)。Range(min=
字符串(索引7、9)。, max=
(索引19、21)。)
文字(索引31、33)来关闭括号。StringBuilder
实例并返回它(索引36、39)。基本上,数据类中的属性越多,字节码越长,因此启动时间也越长。
让我们case class
用Scala 编写等效的代码:
乍一看,Scala似乎生成了一个简单得多的toString
实现:
但是,toString
调用scala.runtime.ScalaRunTime._toString
static方法。依次调用该productIterator
方法来迭代此产品类型。该迭代器调用productElement
方法,如下所示:
基本上可以切换的所有属性case class
。例如,如果productIterator
想要第一个属性,则返回min
。同样,当需要productIterator
第二个元素时,它将返回该max
值。否则,它将抛出一个实例,IndexOutOfBoundsException
以发出超出范围的请求。
同样,我们在中拥有的属性越多case class
,我们将拥有更多的切换臂。因此,字节码的长度与属性的数量成正比。换句话说,与Kotlin的问题相同data class
。
让我们仔细看看为Java记录生成的字节码:
无论记录分量的数量如何,这都是字节码。简单,精致,优雅的解决方案。但是这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方法。
JVM第一次看到invokedynamic
指令时,会调用一个称为Bootstrap Method的特殊静态方法。bootstrap方法是一段我们编写的Java代码,用于准备实际的调用 逻辑:
然后bootstrap方法返回的实例java.invoke.CallSite
。这CallSite
引用了实际方法,即MethodHandle
。从现在开始,每次JVM invokedynamic
再次看到该指令时,它都会跳过Slow Path并直接调用基础可执行文件。除非有所更改,否则JVM会继续跳过慢速路径。
与反射API相对,该java.lang.invoke
API非常有效,因为JVM可以完全查看所有调用。因此,只要我们尽可能避免缓慢的路径,JVM可能会应用各种优化!
除了效率方面的论点之外,由于其简单性,该invokedynamic
方法更可靠,更不易碎。
此外,为Java记录生成的字节码与属性的数量无关。因此,更少的字节码和更快的启动时间。
最后,我们假设Java的新版本包括一个新的且更有效的bootstrap方法实现。使用invokedynamic
,我们的应用无需重新编译即可利用此改进。这样,我们就具有某种正向二进制兼容性。另外,这就是我们正在讨论的动态策略 !
既然我们已经对Indy足够熟悉,那么让我们了解一下invokedynamic
Records字节码:
看看我在Bootstrap方法表中找到的内容:
因此,将调用Records 的引导方法,该方法bootstrap
位于java.lang.runtime.ObjectMethods
类中。如您所见,此引导程序方法需要以下参数:
MethodHandles.Lookup
表示查找上下文的实例(该Ljava/lang/invoke/MethodHandles$Lookup
部分)。toString
,equals
,hashCode
等等)引导将要链接。例如,当值为时toString
,bootstrap将返回一个ConstantCallSite
(CallSite
永远不变)的指针,该指针指向toString
该特定Record 的实际实现。TypeDescriptor
的方法(Ljava/lang/invoke/TypeDescriptor
部分)。Class>
,代表Record类的类型。这是Class
在这种情况下。min;max
。MethodHandle
每个组件一个。这样,bootstrap方法就可以MethodHandle
基于该特定方法实现的组件创建一个。该invokedynamic
指令将所有这些参数传递给bootstrap方法。Bootstrap方法又返回的实例ConstantCallSite
。这ConstantCallSite
持有对请求的方法实现的引用,例如toString
。
对该java.lang.Class
API进行了改进以支持Records。例如,给定a Class>
,我们可以使用new isRecord
方法检查它是否为Record :
它显然会返回false
非记录类型:
还有一种getRecordComponents
方法,该方法RecordComponent
以与原始记录中定义的顺序相同的顺序返回数组。每个java.lang.reflect.RecordComponent
代表一个记录组件或当前记录类型的变量。例如,RecordComponent.getName
返回组件名称:
以相同的方式,该getType
方法为每个组件返回类型令牌:
甚至可以通过getAccessor
以下方式获取访问器方法的句柄:
Java允许您注释记录,只要注释适用于记录或其成员即可。此外,还会有一个ElementType
名为的新注释RECORD_COMPONENT
。此目标的注释只能在记录组件上使用:
任何与序列化无关的新Java功能都是不完整的。但这一次,这种关系听起来并不像我们过去那样令人讨厌。
尽管记录默认情况下不是可序列化的,但是可以通过实现java.io.Serializable
标记接口来使它们成为可能。
可序列化记录的序列化和反序列化与普通可序列化对象不同。更新后的javadoc的ObjectInputStream
规定:
writeObject
,readObject
,readObjectNoData
,writeExternal
,和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等习资料和视频课程干货”。欢迎交流!