跳转到内容

JEP 359:记录(预览)

原文:https://openjdk.org/jeps/359
翻译:张欢

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

动机与目标

人们普遍抱怨“Java太冗长”或“形式过多”。一些最严重的反例是那些仅充当简单“数据载体”的类。为了正确地编写数据载体类,必须编写许多低能的、重复的、易错的代码:构造器、访问器、equals()hashCode()toString()等。开发者有时会想要偷个懒,省略这些重要的方法(导致意外行为或难于调试),或者用一个并不完全合适的类投入服务(因为它具有“正确的形状”,且他们不想再另外声明个类)。

IDE将帮助在数据载体类中写出大部分代码,但不会做任何事情来帮助代码阅读者在几十行样板代码中提取“我是xyz的数据载体”的含义。编写简单聚合建模的Java代码,应该更容易地——写、读和验证正确性。

表面看来,用记录来减少样板代码很诱人,但我们选择了一个更加语义化的目标:用数据为数据建模。(如果语义正确,样板代码会自解释。)声明表面不可变的、行为良好的名义数据聚合应该非常容易、清楚和简洁。

非目标

宣告“样板战争”不是目标;特别是,使用JavaBean命名规范来解决可变类的问题不是目标。添加如属性、元编程、和注解驱动的代码生成也不是目标,即使它们经常被提议作为此问题的“解决方案”。

描述

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

记录具有名称和状态描述。状态描述声明记录的组件。可选地,记录有一个代码体。例如:

record Point(int x, int y) { }

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

  • 状态描述中每个组件的private final字段;
  • 状态描述中每个组件的public读取访问器方法,具有与该组件相同的名称和类型;
  • 一个public的构造器,其签名与状态描述相同,并根据响应的参数为每个字段初始化;
  • 用来表示两条记录相同的equalshashCode实现,如果它们有相同的类型且包含相同状态的话;
  • toString的实现,包括所有记录组件的字符串表示形式及其名称。

换句话说,记录的表示是从状态描述中机械地完全导出的,遵从构造、解构(起初是访问器,当我们具有模式匹配时是解构模式)、相等性和表示的协定。

记录的限制

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

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

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

除了上述限制之外,记录的行为类似于普通类:它们可以声明为顶级或嵌套的,可以是泛型的,可以实现接口,并且可以通过new关键字实例化。记录的代码体可以声明静态方法、静态字段、静态代码块、构造器、实例方法和嵌套类型。记录本身与其状态描述中的单独组件都可以使用注解。如果记录是嵌套的,那么它是隐式静态的;这避免了立即封闭的实例,该实例会静默地将状态添加到记录中。

显式声明记录成员

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

显式声明典型构造器(签名与记录的状态描述相同)有特殊考虑。可以在没有正式参数列表的情况下声明构造器(此时假定与状态描述相同),并且在构造方法体正常完成时,所有未赋值的记录字段都会用对应的形式参数初始化(this.x = x)。这允许显式的典型构造器仅执行其参数的验证和规范化,并省略显式的字段初始化。例如:

record Range(int lo, int hi) {
public Range {
if (lo > hi) /* 这里指的是隐式构造函数参数 */
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}

语法

记录声明:
{类修饰符} record 类型标识符 [类型参数]
(记录组件) [父接口] [记录体]
记录组件:
{记录组件 {, 记录组件}}
记录组件:
{注解} UnannType 标识符
记录体:
{ {记录体声明} }
记录体声明:
类体声明
记录构造器声明
记录构造器声明:
{注解} {构造器修饰符} [类型参数] 简单类型名
[Throws] 构造器体

记录组件的注解

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

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

反射API

下面的public方法将被添加到java.lang.Class中:

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

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

如果给定的类是以记录声明的,那么isRecord()方法返回true。(就像isEnum()。)

备选方案

记录可以视为元组的名义形式。作为记录的替代,我们可以实现结构化元组。但是,虽然元组可以提供表示某些聚合的轻量级方法,但结果通常是劣等的聚合:

  • Java的核心理念是名称很重要。类及其成员具有有意义的名称,而元组和元组的组件却没有。也就是说,具有firstNamelastName属性的Person类,要比具有StringString的匿名元组更为清晰和安全。
  • 类通过构造器支持状态验证;元组则不然。一些数据聚合(如数字范围)具有不变量,如果由构造器强制执行,则以后可以依赖这些不变量;元组则不提供此功能。
  • 类可以具有基于其状态的行为。将状态和行为并列放置可以使行为更易于发现和访问。元组是原始数据,不提供此类功能。

依赖

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