JEP 384:记录(第二次预览)
概括
使用记录增强 Java 编程语言,记录是充当不可变数据的透明载体的类。记录可以被认为是_名义元组_。这是JDK 15 中的预览语言功能。
历史
记录由JEP 359于 2019 年中提出,并于 2020 年初在JDK 14中作为预览功能提供。此 JEP 建议重新预览 JDK 15 中的功能,既要合并基于反馈的改进,又要支持 Java 语言中其他形式的本地类和接口。
目标
- 设计一个面向对象的构造来表达简单的值聚合。
- 帮助程序员专注于对不可变数据进行建模,而不是对可扩展行为进行建模。
- 自动实现数据驱动的方法,例如
equals
和 访问器。 - 保留长期存在的 Java 原则,例如名义类型和迁移兼容性。
非目标
-
向“样板文件宣战”并不是目标。特别是,它的目标不是解决使用 JavaBeans 命名约定的可变类的问题。
-
添加属性或注释驱动的代码生成等功能并不是我们的目标,这些功能通常是为了简化“普通旧 Java 对象”的类声明而提出的。
动机
人们普遍抱怨“Java 太冗长”或“太多仪式”。一些最严重的罪犯是一些类,它们只不过是少数值的不可变_数据载体_。正确编写数据载体类涉及大量低价值、重复、容易出错的代码:构造函数、访问器、、、、equals
等。例如,一个携带 x 和 y 坐标的类不可避免地会hashCode
像toString
这样结束:
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int x() { return x; }
int y() { return y; }
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return other.x == x && other.y = y;
}
public int hashCode() {
return Objects.hash(x, y);
}
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
开发人员有时会试图通过省略诸如 之类的方法来走捷径,equals
从而导致令人惊讶的行为或可调试性较差,或者通过将替代但不完全合适的类压入服务,因为它具有“正确的形状”并且他们还不想声明另一堂课。
IDE 有助于_编写_数据载体类中的大部分代码,但无法帮助读者_从_数十行样板文件中提炼出“我是x
、y
、 和的数据载体”的设计意图。z
编写对少数值进行建模的 Java 代码应该更容易编写、阅读和验证其正确性。
虽然表面上很容易将记录视为主要是为了减少样板文件,但我们选择了一个更具语义的目标:将数据建模为数据。 (如果语义正确,样板文件将自行处理。)声明数据载体类应该简单而简洁,_默认情况下_使其数据不可变,并提供生成和使用数据的方法的惯用实现。
描述
记录_是 Java 语言中的一种新类。记录的目的是声明一小组变量将被视为一种新的实体。记录声明其_状态(变量组)并提交给与该状态匹配的 API。这意味着记录放弃了类通常享有的自由——将类的 API 与其内部表示解耦的能力——但作为回报,记录变得更加简洁。
记录的声明指定名称、标头和主体。标头列出了记录的_组成部分_,它们是构成记录状态的变量。 (组件列表有时称为_状态描述_。)例如:
record Point(int x, int y) { }
由于记录在语义上声称是其数据的透明载体,因此记录会自动获取许多标准成员:
-
对于标头中的每个组件,有两个成员:一个
public
与组件同名、返回类型的访问器方法,以及一个private
final
与组件同类型的字段; -
一个_规范的构造函数_,其签名与标头相同,并将每个字段分配给实例化记录的表达式
private
中的相应参数;new
-
equals
以及hashCode
如果两个记录具有相同类型且包含相同分量值则表示它们相等的方法;和 -
toString
返回所有记录组件及其名称的字符串表示形式的方法。
换句话说,记录的标头描述了其状态(其组件的类型和名称),并且 API 是为该状态描述而机械且完整地导出的。 API 包括用于构造、成员访问、平等和显示的协议。 (我们预计未来版本将支持解构模式,以实现强大的模式匹配。)
记录规则
private
可以显式声明从标头自动派生的任何成员(从记录组件派生的字段除外) 。访问器或equals
/的任何显式实现hashCode
都应小心保留记录的语义不变量。
记录中的构造函数规则与普通类中的构造函数规则不同。没有任何构造函数声明的普通类会自动指定一个_默认构造函数。相反,没有任何构造函数声明的记录会自动获得一个_规范构造函数,该构造函数将所有字段分配给实例化该记录的表达式private
的相应参数。new
例如,之前声明的记录 -- record Point(int x, int y) { }
-- 被编译为:
record Point(int x, int y) {
// Implicitly declared fields
private final int x;
private final int y;
// Other implicit declarations elided ...
// Implicitly declared canonical constructor
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
规范构造函数可以使用与记录头匹配的形式参数列表显式声明,如上所示,也可以以更紧凑的形式声明,以帮助开发人员专注于验证和规范化参数,而无需进行繁琐的参数分配工作到字段。_紧凑的规范构造函数_消除了形式参数列表;它们是隐式声明的,记录组件对应的字段不能在主体中赋值,而是在构造函数末尾private
自动赋值给相应的形参( )。this.x = x;
例如,这是一个紧凑的规范构造函数,用于验证其(隐式)形式参数:
record Range(int lo, int hi) {
Range {
if (lo > hi) // referring here to the implicit constructor parameters
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}
记录声明有许多限制:
-
记录没有
extends
子句。记录的超类总是java.lang.Record
,类似于枚举的超类总是java.lang.Enum
。尽管普通类可以显式扩展其隐式超类Object
,但记录不能显式扩展任何类,甚至不能显式扩展其隐式超类Record
。 -
记录是隐式的
final
,并且不可能是隐式的abstract
。这些限制强调记录的 API 仅由其状态描述定义,并且以后不能通过其他类或记录来增强。 -
记录不能显式声明实例字段,也不能包 含实例初始值设定项。这些限制确保记录头单独定义记录值的状态。
-
与记录类的记录组件相对应的隐式声明字段可以
final
通过反射进行修改(这样做会抛出异常IllegalAccessException
)。这些限制体现了广泛适用于数据载体类别的_不可变的默认策略。_ -
否则自动派生的成员的任何显式声明都必须与自动派生成员的类型完全匹配,而忽略显式声明上的类型注释。
-
记录不能声明
native
方法。如果记录可以声明native
方法,那么根据定义,记录的行为将取决于外部状态而不是记录的显式状态。任何带有方法的类都不native
是迁移到记录的良好候选者。
除了上述限制之外,记录的行为与普通类类似:
-
记录是用
new
关键字实例化的。 -
记录可以声明为顶级或嵌套,并且可以是通用的。
-
记录可以声明静态方法、静态字段和静态初始值设定项。
-
记录可以声明实例方法。即记录可以显式声明
public
组件对应的访问器方法,也可以声明其他实例方法。 -
记录可以实现接口。虽然记录不能指定超类(因为这意味着继承状态,超出标头中描述的状态),但记录可以自由指定超接口并声明实例方法来帮助实现它们。就像类一样,接口可以有效地描述许多记录的行为。该行为可以是与域无关的(例如, )或特定于域的,在这种情况下,记录可以是捕获域的_密封_
Comparable
层次结构的一部分(见下文)。 -
记录可以声明嵌套类型,包括嵌套记录。如果记录本身是嵌套的,则它是_隐式静态的_;这避免了立即封闭的实例会默默地将状态添加到记录中。