跳到主要内容

JEP 359:记录(预览)

概括

通过记录增强 Java 编程语言。记录提供了一种紧凑的语法来声明类,这些类是浅层不可变数据的透明持有者。这是JDK 14 中的预览语言功能。

动机和目标

人们普遍抱怨“Java 太冗长”或者“仪式感”太多。一些最严重的罪犯是一些类,它们只不过是充当简单聚合的普通“数据载体”。为了正确地编写一个数据载体类,必须编写大量低价值、重复、容易出错的代码:构造函数、访问器、、、、equals()等等hashCode()toString()开发人员有时会想走捷径,例如省略这些重要的方法(导致令人惊讶的行为或较差的可调试性),或者将一个替代但不完全合适的类投入使用(因为它具有“正确的形状”并且他们不想声明另一个类)。

IDE 将帮助_编写_数据载体类中的大部分代码,但不会做任何事情来帮助读者_从_数十行样板文件中提取“我是xy、 和的数据载体”的设计意图。z编写对简单聚合进行建模的 Java 代码应该更容易编写、读取和验证是否正确。

虽然表面上很容易将记录视为主要是为了减少样板文件,但我们选择了一个更具语义的目标:将数据建模为数据。 (如果语义正确,样板文件会自行处理。)声明浅层不可变、行为良好的名义数据聚合应该是简单、清晰和简洁的。

非目标

“向样板文件宣战”并不是目标;而是要向样板文件宣战。特别是,它的目标不是使用 JavaBean 命名约定来解决可变类的问题。添加属性、元编程和注释驱动的代码生成等功能并不是我们的目标,尽管它们经常被提议作为该问题的“解决方案”。

描述

_记录_是 Java 语言中的一种新型类型声明。与 an 一样enum,arecord是类的受限形式。它声明其表示形式,并提交与该表示形式相匹配的 API。记录放弃了类通常享有的自由:将 API 与表示分离的能力。作为回报,记录获得了显着的简洁性。

记录具有名称和状态描述。状态描述声明记录的_组成部分_。可选地,记录具有主体。例如:

record Point(int x, int y) { }

由于记录在语义上声称是其数据的简单、透明的持有者,因此记录会自动获取许多标准成员:

  • 状态描述的每个组成部分的私有最终字段;
  • 状态描述的每个组件的公共读取访问器方法,与组件具有相同的名称和类型;
  • 一个公共构造函数,其签名与状态描述相同,它从相应的参数初始化每个字段;
  • equals和的实现hashCode表示,如果两条记录具有相同类型且包含相同状态,则它们相等;和
  • 其实现toString包括所有记录组件及其名称的字符串表示形式。

换句话说,记录的表示完全从状态描述中机械地派生出来,构造、解构(最初的访问器,以及当我们有模式匹配时的解构模式)、相等和显示的协议也是如此。

对记录的限制

记录不能扩展任何其他类,并且不能声明除与状态描述的组件相对应的私有最终字段之外的实例字段。声明的任何其他字段都必须是静态的。这些限制确保状态描述单独定义表示。

记录是隐式最终的,并且不能是抽象的。这些限制强调记录的 API 仅由其状态描述定义,并且以后不能通过其他类或记录来增强。

记录的组成部分是隐式最终的。此限制体现了广泛适用于数据聚合的_不可变的默认策略。_

除了上述限制之外,记录的行为类似于普通类:它们可以声明为顶级或嵌套,它们可以是通用的,它们可以实现接口,并且它们通过关键字实例化new。记录的主体可以声明静态方法、静态字段、静态初始值设定项、构造函数、实例方法和嵌套类型。可以对记录和状态描述中的各个组件进行注释。如果记录是嵌套的,则它是隐式静态的;这避免了立即封闭的实例会默默地将状态添加到记录中。

显式声明记录成员

也可以显式声明从状态描述自动派生的任何成员。然而,不小心实现访问器或equals/hashCode可能会破坏记录的语义不变量。

对于显式声明规范构造函数(其签名与记录的状态描述匹配的构造函数)提供了特殊考虑。构造函数可以在没有形式参数列表的情况下声明(在这种情况下,假设与状态描述相同),并且当构造函数主体正常完成时_明确未分配的_this.x = x任何记录字段都从其相应的形式参数( )隐式初始化出口。这允许显式规范构造函数仅执行其参数的验证和规范化,并省略明显的字段初始化。例如:

record Range(int lo, int hi) {
public Range {
if (lo > hi) /* referring here to the implicit constructor parameters */
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}

语法

RecordDeclaration:
{ClassModifier} record TypeIdentifier [TypeParameters]
(RecordComponents) [SuperInterfaces] [RecordBody]

RecordComponents:
{RecordComponent {, RecordComponent}}

RecordComponent:
{Annotation} UnannType Identifier

RecordBody:
{ {RecordBodyDeclaration} }

RecordBodyDeclaration:
ClassBodyDeclaration
RecordConstructorDeclaration

RecordConstructorDeclaration:
{Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName
[Throws] ConstructorBody

记录组件上的注释

如果声明注释适用于记录组件、参数、字段或方法,则允许在记录组件上使用声明注释。适用于任何这些目标的声明注释将传播到任何强制成员的隐式声明。

修改记录组件类型的类型注释被传播到强制成员的隐式声明中的类型(例如,构造函数参数、字段声明和方法声明)。强制成员的显式声明必须与相应记录组件的类型完全匹配,不包括类型注释。

反射API

以下公共方法将添加到java.lang.Class

  • RecordComponent[] getRecordComponents()
  • boolean isRecord()

该方法getRecordComponents()返回一个对象数组java.lang.reflect.RecordComponent,其中java.lang.reflect.RecordComponent是一个新类。该数组的元素对应于记录的组件,其顺序与记录声明中出现的顺序相同。可以从数组中的每个元素中提取附加信息RecordComponent,包括其名称、类型、泛型类型、注释及其访问器方法。

isRecord()如果给定的类被声明为记录,则该方法返回 true。 (与之比较isEnum()。)

备择方案

_记录可以被视为元组_的名义形式。我们可以实现结构元组,而不是记录。然而,虽然元组可能提供表达某些聚合的轻量级方法,但结果通常是较差的聚合:

  • Java 哲学的一个核心方面是_名称很重要_。类及其成员具有有意义的名称,而元组和元组组件则没有。也就是说,Person具有属性 和 的类比firstName和的lastName匿名元组更清晰、更安全。String``String

  • 类通过其构造函数支持状态验证;元组则不然。某些数据聚合(例如数值范围)具有不变量,如果由构造函数强制执行,则此后可以依赖这些不变量;元组不提供这种能力。

  • 类可以具有基于其状态的行为;将状态和行为放在一起使得行为更容易发现并且更容易访问。元组作为原始数据,不提供这样的功能。

依赖关系

记录与密封类型配合良好(JEP 360);记录和密封类型一起形成通常称为代数数据类型的构造。此外,记录很自然地适合模式匹配。由于记录将其 API 与其状态描述耦合在一起,因此我们最终也能够导出记录的解构模式,并使用密封类型信息来确定switch具有类型模式或解构模式的表达式的详尽性。