JEP 359:记录(预览)

官方原文(英文)地址: https://openjdk.java.net/jeps/359
个人原创翻译,转载请注明出处。

Summary

Enhance the Java programming language with records. Records provide a compact syntax for declaring classes which are transparent holders for shallowly immutable data. This is a preview language feature in JDK 14.

摘要

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

Motivation and Goals

It is a common complaint that "Java is too verbose" or has too much "ceremony". Some of the worst offenders are classes that are nothing more than plain "data carriers" that serve as simple aggregates. To write a data carrier class properly, one has to write a lot of low-value, repetitive, error-prone code: constructors, accessors, equals(), hashCode(), toString(), etc. Developers are sometimes tempted to cut corners such as omitting these important methods (leading to surprising behavior or poor debuggability), or pressing an alternate but not entirely appropriate class into service (because it has the "right shape" and they don't want to declare yet another class).

动机与目标

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

IDEs will help write most of the code in a data carrier class, but don't do anything to help the reader distill the design intent of "I'm a data carrier for x, y, and z" from the dozens of lines of boilerplate. Writing Java code that models simple aggregates should be easier -- to write, to read, and to verify as correct.

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

While it is superficially tempting to treat records as primarily being about boilerplate reduction, we instead choose a more semantic goal: modeling data as data. (If the semantics are right, the boilerplate will take care of itself.) It should be easy, clear, and concise to declare shallowly-immutable, well-behaved nominal data aggregates.

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

Non-Goals

It is not a goal to declare "war on boilerplate"; in particular, it is not a goal to address the problems of mutable classes using the JavaBean naming conventions. It is not a goal to add features such as properties, metaprogramming, and annotation-driven code generation, even though they are frequently proposed as "solutions" to this problem.

非目标

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

Description

Records are a new kind of type declaration in the Java language. Like an enum, a record is a restricted form of class. It declares its representation, and commits to an API that matches that representation. Records give up a freedom that classes usually enjoy: the ability to decouple API from representation. In return, records gain a significant degree of concision.

描述

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

A record has a name and a state description. The state description declares the components of the record. Optionally, a record has a body. For example:

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

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

Because records make the semantic claim of being simple, transparent holders for their data, a record acquires many standard members automatically:

  • A private final field for each component of the state description;
  • A public read accessor method for each component of the state description, with the same name and type as the component;
  • A public constructor, whose signature is the same as the state description, which initializes each field from the corresponding argument;
  • Implementations of equals and hashCode that say two records are equal if they are of the same type and contain the same state; and
  • An implementation of toString that includes the string representation of all the record components, with their names.

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

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

In other words, the representation of a record is derived mechanically and completely from the state description, as are the protocols for construction, deconstruction (accessors initially, and deconstruction patterns when we have pattern matching), equality, and display.

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

Restrictions on records

Records cannot extend any other class, and cannot declare instance fields other than the private final fields which correspond to components of the state description. Any other fields which are declared must be static. These restrictions ensure that the state description alone defines the representation.

记录的限制

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

Records are implicitly final, and cannot be abstract. These restrictions emphasize that the API of a record is defined solely by its state description, and cannot be enhanced later by another class or record.

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

The components of a record are implicitly final. This restriction embodies an immutable by default policy that is widely applicable for data aggregates.

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

Beyond the restrictions above, records behave like normal classes: they can be declared top level or nested, they can be generic, they can implement interfaces, and they are instantiated via the new keyword. The record's body may declare static methods, static fields, static initializers, constructors, instance methods, and nested types. The record, and the individual components in a state description, may be annotated. If a record is nested, then it is implicitly static; this avoids an immediately enclosing instance which would silently add state to the record.

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

Explicitly declaring members of a record

Any of the members that are automatically derived from the state description can also be declared explicitly. However, carelessly implementing accessors or equals/hashCode risks undermining the semantic invariants of records.

显式声明记录成员

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

Special consideration is provided for explicitly declaring the canonical constructor (the one whose signature matches the record's state description). The constructor may be declared without a formal parameter list (in this case, it is assumed identical to the state description), and any record fields which are definitely unassigned when the constructor body completes normally are implicitly initialized from their corresponding formal parameters (this.x = x) on exit. This allows an explicit canonical constructor to perform only validation and normalization of its parameters, and omit the obvious field initialization. For example:

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

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

Grammar

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

Annotations on record components

Declaration annotations are permitted on record components if they are applicable to record components, parameters, fields, or methods. Declaration annotations that are applicable to any of these targets are propagated to implicit declarations of any mandated members.

记录组件的注解

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

Type annotations that modify the types of record components are propagated to the types in implicit declarations of mandated members (e.g., constructor parameters, field declarations, and method declarations). Explicit declarations of mandated members must match the type of the corresponding record component exactly, not including type annotations.

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

Reflection API

The following public methods will be added to java.lang.Class:

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

反射API

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

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

The method getRecordComponents() returns an array of java.lang.reflect.RecordComponent objects, where java.lang.reflect.RecordComponent is a new class. The elements of this array correspond to the record’s components, in the same order as they appear in the record declaration. Additional information can be extracted from each RecordComponent in the array, including its name, type, generic type, annotations, and its accessor method.

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

The method isRecord() returns true if the given class was declared as a record. (Compare with isEnum().)

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

Alternatives

Records can be considered a nominal form of tuples. Instead of records, we could implement structural tuples. However, while tuples might offer a lighterweight means of expressing some aggregates, the result is often inferior aggregates:

  • A central aspect of Java's philosophy is that names matter. Classes and their members have meaningful names, while tuples and tuple components do not. That is, a Person class with properties firstName and lastName is clearer and safer than an anonymous tuple of String and String.
  • Classes support state validation through their constructors; tuples do not. Some data aggregates (such as numeric ranges) have invariants that, if enforced by the constructor, can thereafter be relied upon; tuples do not offer this ability.
  • Classes can have behavior that is based on their state; co-locating the state and behavior makes the behavior more discoverable and easier to access. Tuples, being raw data, offer no such facility.

替代方案

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

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

Dependencies

Records go well with sealed types (JEP 360); records and sealed types taken together form a construct often referred to as algebraic data types. Further, records lend themselves naturally to pattern matching. Because records couple their API to their state description, we will eventually be able to derive deconstruction patterns for records as well, and use sealed type information to determine exhaustiveness in switch expressions with type patterns or deconstruction patterns.

依赖

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