JEP 395:记录
概括
使用记录增强 Java 编程语言,记录是充当不可变数据的透明载体的类。记录可以被认为是_名义元组_。
历史
记录由JEP 359提出,并作为预览功能在JDK 14中提供。
为了响应反馈,该设计由JEP 384进行了改进,并第二次作为预览功能在JDK 15中提供。第二次预览的改进如下:
-
在第一个预览中,规范构造函数必须是
public
.在第二个预览中,如果隐式声明规范构造函数,则其访问修饰符与记录类相同;如果显式声明规范构造函数,则其访问修饰符必须至少提供与记录类一样多的访问权限。 -
注释的含义
@Override
被扩展为包括注释方法是记录组件的显式声明的访问器方法的情况。 -
为了强制使用紧凑构造函数,分配给构造函数主体中的任何实例字段都会出现编译时错误。
-
引入了声明本地记录类、本地枚举类和本地接口的功能。
此 JEP 建议在 JDK 16 中最终确定该功能,并进行以下改进:
- 放宽长期存在的限制,即内部类不能声明显式或隐式静态的成员。这将变得合法,特别是允许内部类声明一个记录类成员。
根据进一步的反馈,可以纳入额外的改进。
目标
-
设计一个面向对象的构造来表达简单的 值聚合。
-
帮助开发人员专注于对不可变数据进行建模,而不是对可扩展行为进行建模。
-
自动实现数据驱动的方法,例如
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
编写对少数值进行建模的 Java 代码应该更容易编写、阅读和验证其正确性。
虽然表面上很容易将记录视为主要是为了减少样板文件,但我们选择了一个更具语义的目标:将数据建模为数据。 (如果语义正确,样板文件将自行处理。)声明数据载体类应该简单而简洁,_默认情况下_使其数据不可变,并提供生成和使用数据的方法的惯用实现。
描述
_记录类_是Java 语言中的一种新类。记录类有助于以比普通类更少的仪式来对纯数据聚合进行建模。
记录类的声明主要包括其_状态_的声明;然后记录类提交给与该状态匹配的 API。这意味着记录类放弃了类通常享有的自由——将类的 API 与其内部表示解耦的能力——但作为回报,记录类声明变得更加简洁。
更准确地说,记录类声明由名称、可选类型参数、标头和主体组成。标头列出了记录类的_组件_,它们是构成其状态的变量。 (此组件列表有时称为_状态描述_。)例如:
record Point(int x, int y) { }
由于记录类在语义上声明其数据是透明的载体,因此记录类会自动获取许多标准成员:
-
对于标头中的每个组件,有两个成员:一个
public
与组件同名、返回类型的访问器方法,以及一个private
final
与组件同类型的字段; -
一个_规范的构造函数_,其签名与标头相同,并将每个私有字段分配给
new
实例化记录的表达式中的相应参数; -
equals
以及hashCode
确保两个记录值相等(如果它们具有相同类型且包含相等的分量值)的方法;和 -
toString
返回所有记录组件及其名称的字符串表示形式的方法。
换句话说,记录类的标头描述了它的状态,即其组件的类型和名称,并且API完全从该状态描述中机械地派生出来。 API 包括用于构造、成员访问、平等和显示的协议。 (我们预计未来版本将支持解构模式,以实现强大的模式匹配。)
记录类的构造函数
记录类中的构造函数的规则与普通类中的不同。没有任何构造函数声明的普通类会自动指定一个默认构造函数。相反,没有任何构造函数声明的记录类会自动获得一个_规范构造函数_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;
}
}
可以使用与记录头匹配的形式参数列表显式声明规范构造函数,如上所示。还可以通过省略形式参数列表来更紧凑地声明它。在这样一个_紧凑的规范构造函数_中,参数是隐式声明的,与记录组件对应的私有字段不能在主体中分配,而是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));
}
}
这是一个紧凑的规范构造函数,它规范了其形式参数:
record Rational(int num, int denom) {
Rational {
int gcd = gcd(num, denom);
num /= gcd;
denom /= gcd;
}
}
此声明相当于传统的构造函数形式:
record Rational(int num, int denom) {
Rational(int num, int demon) {
// Normalization
int gcd = gcd(num, denom);
num /= gcd;
denom /= gcd;
// Initialization
this.num = num;
this.denom = denom;
}
}
具有隐式声明的构造函数和方法的记录类满足重要且直观的语义属性。例如,考虑声明R
如下的记录类:
record R(T1 c1, ..., Tn cn){ }
如果通过以下方式复制r1
的实例:R
R r2 = new R(r1.c1(), r1.c2(), ..., r1.cn());
那么,假设r1
不是空引用,则表达式的计算结果_总是_为。显式声明的访问器和方法应遵守此不变式。但是,编译器通常不可能检查显式声明的方法是否遵守此不变式。r1.equals(r2)``true``equals
举个例子,下面的记录类声明应该被认为是不好的风格,因为它的访问器方法“默默地”调整记录实例的状态,并且不满足上面的不变量:
record SmallPoint(int x, int y) {
public int x() { return this.x < 100 ? this.x : 100; }
public int y() { return this.y < 100 ? this.y : 100; }
}
此外,对于所有记录类,equals
都会实现隐式声明的方法,以便它是自反的,并且其行为与hashCode
具有浮点组件的记录类一致。同样,显式声明equals
和hashCode
方法的行为应该类似。
记录等级规则
与普通类相比,记录类的声明有许多限制:
-
记录类声明没有
extends
子句。记录类的超类总是java.lang.Record
,类似于枚举类的超类总是java.lang.Enum
。尽管普通类可以显式扩展其隐式超类Object
,但记录不能显式扩展任何类,甚至是其隐式超类Record
。 -
记录类是隐式的
final
,并且不能是隐式的abstract
。这些限制强调记录类的 API 仅由其状态描述定义,并且 以后不能由其他类增强。 -
从记录组件派生的字段是
final
。此限制体现了广泛适用于数据载体类别的_不可变的默认策略。_ -
记录类不能显式声明实例字段,也不能包含实例初始值设定项。这些限制确保记录头单独定义记录值的状态。
-
否则自动派生的成员的任何显式声明都必须与自动派生成员的类型完全匹配,而忽略显式声明上的任何注释。访问器或
equals
或方法的任何显式实现hashCode
都应小心保留记录类的语义不变量。 -
记录类不能声明
native
方法。如果记录类可以声明一个native
方法,那么根据定义,记录类的行为将取决于外部状态而不是记录类的显式状态。没有具有本机方法的类可能是迁移到记录的良好候选者。
除了上述限制之外,记录类的行为与普通类类似:
-
记录类的实例是使用表达式创建的
new
。 -
记录类可以声明为顶级或嵌套,并且可以是通用的。
-
记录类可以声明
static
方法、字段和初始值设定项。 -
记录类可以声明实例方法。
-
记录类可以实现接口。记录类不能指定超类,因为这意味着继承状态,超出标头中描述的状态。然而,记录类可以自由地指定超级接口并声明实例方法来实现它们。就像类一样,接口可以有效地描述许多记录的行为。该行为可以是与域无关的(例如, )或特定于域的,在这种情况下,记录可以是捕获域的_密封_
Comparable
层次结构的一部分(见下文)。 -
记录类可以声明嵌套类型,包括嵌套记录类。如果记录类本身是嵌套的,那么它是隐式静态的;这避免了立即封闭的实例会默默地将状态添加到记录类中。
-
记录类及其标头中的组件可以用注释来修饰。根据注释的适用目标集,记录组件上的任何注释都会传播到自动派生的字段、方法和构造函数参数。记录组件类型上的类型注释也会传播到自动派生成员中的相应类型使用。
-
记录类的实例可以序列化和反序列化。但是,不能通过提供
writeObject
、readObject
、readObjectNoData
、writeExternal
、 或方法来自定义流程readExternal
。记录类的组件控制序列化,而记录类的规范构造函数控制反序列化。
本地记录类
生成和使用记录类实例的程序可能会处理许多中间值,这些中间值本身就是简单的变量组。声明记录类来对这些中间值建模通常很方便。一种选择是声明静态且嵌套的“帮助器”记录类,就像当今许多程序声明帮助器类一样。更方便的选择是_在方法内_声明一条记录,靠近操作变量的代码。因此,我们定义_本地记录类_,类似于本地类的现有构造。
在以下示例中,商家和每月销售额的聚合是使用本地记录类 建模的MerchantSales
。使用此记录类可以提高以下流操作的可读性:
List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
// Local record
record MerchantSales(Merchant merchant, double sales) {}
return merchants.stream()
.map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
.sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
.map(MerchantSales::merchant)
.collect(toList());
}
本地记录类是嵌套记录类的特例。与嵌套记录类一样,本地记录类是_ 隐式静态的_。这意味着它们自己的方法不能访问封闭方法的任何变量;反过来,这可以避免捕获立即封闭的实例,该实例会默默地将状态添加到记录类中。本地记录类是隐式静态的这一事实与本地类相反,本地类不是隐式静态的。事实上,局部类从来都不是静态的——无论是隐式的还是显式的——并且总是可以访问封闭方法中的变量。
本地枚举类和本地接口
添加本地记录类是添加其他类型的隐式静态本地声明的机会。
嵌套枚举类和嵌套接口已经是隐式静态的,因此为了保持一致性,我们定义了本地枚举类和本地接口,它们也是隐式静态的。
内部类的静态成员
当前,如果内部类声明显式或隐式静态的成员,则指定为编译时错误,除非该成员是常量变量。这意味着,例如,内部类不能声明记录类成员,因为嵌套记录类是隐式静态的。
我们放宽此限制,以便允许内部类声明显式或隐式静态的成员。特别是,这允许内部类声明作为记录类的静态成员。