跳到主要内容

JEP 384:记录(第二次预览)

概括

使用记录增强 Java 编程语言,记录是充当不可变数据的透明载体的类。记录可以被认为是_名义元组_。这是JDK 15 中的预览语言功能。

历史

记录由JEP 359于 2019 年中提出,并于 2020 年初在JDK 14中作为预览功能提供。此 JEP 建议重新预览 JDK 15 中的功能,既要合并基于反馈的改进,又要支持 Java 语言中其他形式的本地类和接口。

目标

  • 设计一个面向对象的构造来表达简单的值聚合。
  • 帮助程序员专注于对不可变数据进行建模,而不是对可扩展行为进行建模。
  • 自动实现数据驱动的方法,例如equals和 访问器。
  • 保留长期存在的 Java 原则,例如名义类型和迁移兼容性。

非目标

  • 向“样板文件宣战”并不是目标。特别是,它的目标不是解决使用 JavaBeans 命名约定的可变类的问题。

  • 添加属性或注释驱动的代码生成等功能并不是我们的目标,这些功能通常是为了简化“普通旧 Java 对象”的类声明而提出的。

动机

人们普遍抱怨“Java 太冗长”或“太多仪式”。一些最严重的罪犯是一些类,它们只不过是少数值的不可变_数据载体_。正确编写数据载体类涉及大量低价值、重复、容易出错的代码:构造函数、访问器、、、、equals等。例如,一个携带 x 和 y 坐标的类不可避免地会hashCodetoString这样结束:

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 有助于_编写_数据载体类中的大部分代码,但无法帮助读者_从_数十行样板文件中提炼出“我是xy、 和的数据载体”的设计意图。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层次结构的一部分(见下文)。

  • 记录可以声明嵌套类型,包括嵌套记录。如果记录本身是嵌套的,则它是_隐式静态的_;这避免了立即封闭的实例会默默地将状态添加到记录中。

  • 可以对记录及其状态描述中的组件进行注释。注释将传播到自动派生的字段、方法和构造函数参数。记录组件类型上的类型注释也会传播到自动派生成员的类型。

记录和密封类型

记录与密封类型JEP 360 )配合良好。例如,一系列记录可以实现相同的密封接口:

package com.example.expression;

public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}

public record ConstantExpr(int i) implements Expr {...}
public record PlusExpr(Expr a, Expr b) implements Expr {...}
public record TimesExpr(Expr a, Expr b) implements Expr {...}
public record NegExpr(Expr e) implements Expr {...}

记录和密封类型的组合有时称为代数数据类型。记录允许我们表达_产品类型_,密封类型允许我们表达_总和类型_。

本地记录

生成和使用记录的程序可能会处理许多中间值,这些中间值本身就是简单的变量组。声明记录来对这些中间值建模通常很方便。一种选择是声明static嵌套的“帮助器”记录,就像当今许多程序声明帮助器类一样。更方便的选择是_在方法内_声明一条记录,靠近操作变量的代码。因此,这个 JEP 提出了_本地记录_,类似于本地类的传统构造。

在以下示例中,商家和每月销售额的聚合是使用本地记录 建模的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());
}

本地记录是嵌套记录的特例。与所有嵌套记录一样,本地记录是_隐式静态的_。这意味着它们自己的方法不能访问封闭方法的任何变量;反过来,这可以避免捕获立即封闭的实例,该实例会默默地将状态添加到记录中。本地记录是隐式静态的这一事实与本地类形成鲜明对比,本地类不是隐式静态的。事实上,局部类从来都不是静态的——无论是隐式的还是显式的——并且总是可以访问封闭方法中的变量。

鉴于本地记录的有用性,拥有本地枚举和本地接口也将很有用。传统上,由于担心它们的语义,它们在 Java 中是不允许的。具体来说,嵌套枚举和嵌套接口是隐式静态的,因此本地枚举和本地接口也应该是隐式静态的;然而,Java 语言中的局部声明(局部变量、局部类)从来都不是静态的。然而, JEP 359中本地记录的引入克服了这种语义问题,允许本地声明是静态的,并为本地枚举和本地接口打开了大门。

记录上的注释

记录组件在记录声明中具有多种作用。记录组件是一个一流的概念,但每个组件还对应一个同名和类型的字段、同名和返回类型的访问器方法以及同名和类型的构造函数参数。

这就提出了一个问题,当一个组件被注释时,实际上注释的是什么?答案是,“所有这些都适用于这个特定的注释。”这使得在其字段、构造函数参数或访问器方法上使用注释的类能够迁移到记录,而无需多余地声明这些成员。例如,如下所示的类

public final class Card {
private final @MyAnno Rank rank;
private final @MyAnno Suit suit;
@MyAnno Rank rank() { return this.rank; }
@MyAnno Suit suit() { return this.suit; }
...
}

可以迁移到等效的、可读性更高的记录声明:

public record Card(@MyAnno Rank rank, @MyAnno Suit suit) { ... }

注释的适用性是使用@Target元注释来声明的。考虑以下:

@Target(ElementType.FIELD)
public @interface I1 {...}

这声明了注释@I1并且它适用于字段声明。我们可以声明一个注解适用于多个声明;例如:

@Target({ElementType.FIELD, ElementType.METHOD})
public @interface I2 {...}

这声明了一个注释@I2,并且它适用于字段声明和方法声明。

返回到记录组件上的注释,这些注释出现在它们适用的相应程序点处。换句话说,传播是在程序员使用@Target元注释的控制之下。传播规则系统且直观,并且遵循所有适用的规则:

  • 如果记录组件上的注释适用于字段声明,则该注释将出现在相应的private字段上。

  • 如果记录组件上的注释适用于方法声明,则该注释将出现在相应的访问器方法上。

  • 如果记录组件上的注释适用于形式参数,则注释将出现在规范构造函数的相应形式参数上(如果未显式声明),或者出现在紧凑构造函数的相应形式参数上(如果显式声明)。

  • 如果记录组件上的注释适用于类型,则传播规则与声明注释相同,只是注释出现在相应的类型使用而不是声明上。

如果显式声明公共访问器方法或(非紧凑)规范构造函数,则它仅具有直接出现在其上的注释;没有任何内容从相应的记录组件传播到这些成员。

还可以使用新的注释声明来声明注释来自记录组件上定义的注释@Target(RECORD_COMPONENT)。这些注释可以通过反射来检索,如下面的反射 API部分所述。

Java语法

RecordDeclaration:
{ClassModifier} `record` TypeIdentifier [TypeParameters]
RecordHeader [SuperInterfaces] RecordBody

RecordHeader:
`(` [RecordComponentList] `)`

RecordComponentList:
RecordComponent { `,` RecordComponent}

RecordComponent:
{Annotation} UnannType Identifier
VariableArityRecordComponent

VariableArityRecordComponent:
{Annotation} UnannType {Annotation} `...` Identifier

RecordBody:
`{` {RecordBodyDeclaration} `}`

RecordBodyDeclaration:
ClassBodyDeclaration
CompactConstructorDeclaration

CompactConstructorDeclaration:
{Annotation} {ConstructorModifier} SimpleTypeName ConstructorBody

类文件表示

class记录的文件使用属性Record来存储有关记录组件的信息:

Record_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 components_count;
record_component_info components[components_count];
}

record_component_info {
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

如果记录组件具有与擦除描述符不同的通用签名,则结构Signature中必须有一个属性record_component_info

反射API

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

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

该方法getRecordComponents()返回一个对象数组java.lang.reflect.RecordComponent。该数组的元素对应于记录的组件,其顺序与记录声明中出现的顺序相同。可以从数组中的每个元素中提取附加信息,包括其名称、注释和访问器方法。

isRecord如果给定的类被声明为记录,则该方法返回 true。 (与之比较isEnum。)

备择方案

_记录可以被视为元组_的名义形式。我们可以实现结构元组,而不是记录。然而,虽然元组可能提供更轻量级的方式来表达某些聚合,但结果通常是较差的聚合:

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

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

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

依赖关系

除了上面提到的记录和密封类型的组合之外,记录本身也适合模式匹配。由于记录将其 API 与其状态描述耦合在一起,因此我们最终也能够导出记录的解构模式,并使用密封类型信息来确定switch具有类型模式或解构模式的表达式的详尽性。