Skip to content

JEP 359: Records (Preview) | 记录(预览版)

摘要

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

动机和目标

人们常常抱怨“Java 太冗长了”或者“仪式性太强了”。其中一些最严重的“罪犯”就是那些仅仅是作为简单聚合体的“数据载体”的类。为了正确地编写一个数据载体类,需要编写大量低价值、重复性且容易出错的代码:构造函数、访问器、equals()hashCode()toString() 等。有时开发人员会试图走捷径,比如省略这些重要的方法(导致意外的行为或难以调试),或者将另一个不太合适的类强行用作服务(因为它有“正确的形状”,而开发人员不想声明另一个类)。

集成开发环境(IDEs)可以帮助 编写 数据载体类中的大部分代码,但对帮助 读者 从数十行样板代码中提炼出“我是 xyz 的数据载体”的设计意图无能为力。编写建模简单聚合体的 Java 代码应该更简单——编写、阅读和验证其正确性。

虽然从表面上看,将 records 视为主要是关于减少样板代码的做法很诱人,但我们选择了更语义化的目标:将数据建模为数据。(如果语义正确,样板代码会自行处理。)声明浅不可变、行为良好的名义数据聚合体应该简单、清晰、简洁。

非目标

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

描述

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

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

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

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

  • 为状态描述中的每个组件提供一个私有的 final 字段;
  • 为状态描述中的每个组件提供一个公共的读取访问器方法,该方法的名称和类型与组件相同;
  • 一个公共的构造函数,其签名与状态描述相同,该函数从相应的参数中初始化每个字段;
  • equalshashCode 的实现,用于判断两个 record 是否相等,如果它们属于相同的类型且包含相同的状态,则它们相等;
  • toString 的实现,它包含了所有 record 组件的字符串表示形式,以及它们的名称。

换句话说,record 的表示形式完全由状态描述机械地推导出来,而构造、解构(最初是访问器,以及我们进行模式匹配时的解构模式)、相等性和显示协议也是如此。

对 record 的限制

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

record 是隐式 final 的,不能是抽象的。这些限制强调了 record 的 API 仅由其状态描述定义,并且不能由另一个类或 record 在稍后进行增强。

record 的组件是隐式 final 的。这一限制体现了 默认不可变 的策略,该策略在数据聚合中广泛适用。

除了上述限制之外,record 的行为与普通类相似:它们可以声明为顶级类或嵌套类,可以是泛型的,可以实现接口,并通过 new 关键字进行实例化。record 的主体可以声明静态方法、静态字段、静态初始化器、构造函数、实例方法和嵌套类型。record 以及状态描述中的各个组件都可以进行注解。如果 record 是嵌套的,那么它默认为静态的;这避免了立即封闭实例会默默地向 record 添加状态。

显式声明 record 的成员

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

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

java
record Range(int lo, int hi) {
  public Range {
    if (lo > hi)  /* 这里引用了隐式构造函数参数 */
      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

record 组件上的注解

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

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

反射 API

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

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

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

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

备选方案

记录可以被认为是 元组 的一种命名形式。除了记录之外,我们还可以实现结构化的元组。但是,虽然元组可能提供了一种更轻量级的方式来表达某些聚合,但结果往往是不够理想的聚合:

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

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

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

依赖项

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