跳到主要内容

JEP 359: 记录(预览版)

QWen Max 中英对照 JEP 359: Records (Preview)

概述

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

动机与目标

一个常见的抱怨是“Java 太啰嗦”或者有太多的“形式主义”。一些最严重的违规者是那些仅仅是普通“数据载体”的类,它们充当简单的聚合体。要正确编写一个数据载体类,必须编写大量低价值、重复且容易出错的代码:构造函数、访问器、equals()hashCode()toString() 等等。开发者有时会忍不住走捷径,比如省略这些重要的方法(导致行为异常或调试困难),或者使用一个替代但不完全合适的类来充数(因为它具有“正确的形状”,他们不想再声明另一个类)。

IDE 会帮助编写数据载体类中的大部分代码,但几十行样板代码对于帮助读者提炼出“我是 xyz 的数据载体”这样的设计意图毫无作用。编写用于对简单聚合进行建模的 Java 代码应该更简单 —— 更易于编写、阅读以及验证其正确性。

虽然表面上看,将记录(records)主要视为减少样板代码的工具是诱人的,但我们选择了一个更具语义性的目标:将数据建模为数据。(如果语义正确,样板代码自然会得到解决。)声明浅层不可变、行为良好的名义数据聚合应当简单、清晰且简洁。

非目标

“对模板代码宣战”并不是一个目标;特别是,使用 JavaBean 命名约定来解决可变类的问题并不是一个目标。添加诸如属性、元编程和注解驱动的代码生成等功能也不是一个目标,尽管它们经常被提议作为这个问题的“解决方案”。

描述

Records 是 Java 语言中一种新型的类型声明。与 enum 类似,record 是一种受限的类形式。它声明了自己的表示形式,并承诺提供与该表示形式相匹配的 API。Records 放弃了类通常享有的一个自由:将 API 与表示形式解耦的能力。作为回报,records 获得了显著的简洁性。

记录有一个名称和状态描述。状态描述声明了记录的 components。可选地,记录有一个主体。例如:

record Point(int x, int y) { }
java

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

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

换句话说,记录的表示形式是根据状态描述机械且完整地推导出来的,构造、解构(最初是访问器,当有模式匹配时是解构模式)、相等性和显示的协议也是如此。

记录的限制

记录不能扩展任何其他类,也不能声明与状态描述组件相对应的 private final 字段以外的实例字段。任何其他声明的字段必须是静态的。这些限制确保了仅状态描述定义了表示形式。

记录类隐式为 final,并且不能是 abstract 的。这些限制强调了记录类的 API 仅由其状态描述定义,并且不能通过其他类或记录类在之后进行扩展。

记录的组件隐式为 final。此限制体现了一种广泛适用于数据聚合的默认不可变策略。

除了上述限制之外,记录的行为类似于普通类:它们可以声明为顶级或嵌套,可以是泛型,可以实现接口,并且通过 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));
}
}
java

语法

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
java

记录组件上的注解

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

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

反射 API

下面这些公共方法将被添加到 java.lang.Class 中:

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

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

方法 isRecord() 在给定的类被声明为记录(record)时返回 true。(与 isEnum() 进行比较。)

替代方案

记录可以被视为一种名义上的 元组 形式。我们可以不使用记录,而是实现结构化的元组。然而,尽管元组可能提供了一种更轻量的方式来表达某些聚合体,但结果往往会导致质量较低的聚合体:

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

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

  • 类可以基于其状态拥有行为;将状态与行为共置一处使得行为更易于发现和访问。元组作为原始数据,不提供这样的功能。

依赖

记录与密封类型(JEP 360)配合得很好;记录和密封类型共同构成了一种通常被称为代数数据类型的结构。此外,记录天然适合模式匹配。因为记录将其 API 与其状态描述耦合在一起,我们最终将能够为记录推导出解构模式,并利用密封类型信息来确定在带有类型模式或解构模式的 switch 表达式中的穷尽性。