JAVA record

引言

record在JAVA LTS17版本中已经发布了。在编码时通常在写如下类时会有相关的提示:
在这里插入图片描述
可以改成:

public record Wizard(String name) {
    
    

    @Override
    public String toString() {
    
    
        return name;
    }
}

原文 https://openjdk.java.net/jeps/395 在谷歌翻译基础上做了一点修改。

概括

使用record增强 Java 编程语言,record是充当不可变数据的透明载体的类。record可以被认为是名义元组。

历史

record由JEP 359提出 并在JDK 14 中作为 预览功能提供。

作为对反馈的回应,JEP 384对该设计进行了改进, 并在JDK 15 中作为第二次预览功能交付 。第二次预览的改进如下:

  • 在第一个预览版中,规范构造函数必须是public. 在第二个预览中,如果隐式声明了规范构造函数,则其访问修饰符与record类相同;如果显式声明了规范构造函数,则其访问修饰符必须提供至少与record类一样多的访问权限。
  • @Override注解的含义被扩展为包括注解方法是record组件的显式声明的访问器方法的情况。
  • 为了强制使用紧凑构造函数,分配给构造函数主体中的任何实例字段会导致编译时错误。
  • 引入了声明本地record类、本地枚举类和本地接口的能力。

该 JEP 建议在 JDK 16 中完成该功能,并进行以下改进:

  • 放宽长期存在的限制,即内部类不能声明显式或隐式静态成员。这将变得合法,特别是将允许内部类声明作为record类的成员。 可以根据进一步的反馈合并其他改进。

目标

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

非目标

  • 虽然record在声明数据载体类时确实提供了改进的简洁性,但宣布“对样板的战争”并不是目标。特别是,解决使用 JavaBeans 命名约定的可变类的问题并不是目标。
  • 添加特性或注解驱动的代码生成等特性并不是目标,这些特性通常被提议用于简化“Plain Old Java Objects”的类声明。

动机

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

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的方法来偷工减料,导致令人惊讶的行为或可调试性差,或者将替代但不完全合适的类压入服务中,因为它具有“正确的形状”并且他们还不想声明另一个类。

集成开发环境帮助我们写的大部分代码的数据载体类,但没有做任何事情来帮助读者提炼出设计意图的“我是一个数据载体x和y”从几十个样板线。编写对少数值建模的 Java 代码应该更容易编写、阅读和验证是否正确。

虽然表面上将record视为主要与样板减少有关,但我们选择了一个更具语义的目标:将数据建模为数据。(如果语义是正确的,样板将自行处理。)声明数据载体类应该简单而简洁,默认情况下,
这些类使它们的数据不可变,并提供生成和使用数据的方法的惯用实现。

描述

record类是 Java 语言中的一种新类。record类有助于用比普通类更少的仪式对普通数据聚合进行建模。

record类的声明主要由其状态的声明组成 ;然后record类提交到与该状态匹配的 API。这意味着record类放弃了类通常享有的自由——将类的 API 与其内部表示分离的能力——但作为回报,record类声明变得更加简洁。

更准确地说,record类声明由名称、可选类型参数、标题和正文组成。标题列出了record类的组件,它们是构成其状态的变量。(此组件列表有时称为状态描述。)例如:

record Point(int x, int y) {
    
    
}

因为record类在语义上声称是其数据的透明载体,所以record类会自动获取许多标准成员:

  • 对于头部中的每个组件,两个成员:一个public与组件同名和返回类型的访问器方法,以及一个private final与组件类型相同的字段;
  • 和标题相同方法签名的规范构造,在new方法调用时将赋予相应的值以初始化对象。
  • equals以及hashCode确保两个record值相同的方法,如果它们是相同的类型并且包含相同的组件值。
  • toString返回所有record组件的字符串表示形式及其名称的方法。

换句话说,record类的头部描述了它的状态,即它的组件的类型和名称,而 API 是从该状态描述中机械地和完全地派生出来的。API 包括用于构建、成员访问、平等和显示的协议。(我们希望未来的版本支持解构模式,以实现强大的模式匹配。)

record类的构造函数

record类中构造函数的规则与普通类不同。没有任何构造函数声明的普通类会自动获得一个
默认构造函数。相比之下,没有任何构造函数声明的record类会自动获得一个规范的构造函数,该构造函数将所有私有字段分配给new实例化record的表达式的相应参数。例如,前面声明的recordrecord 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;
    }
}

可以使用与record头匹配的形式参数列表显式声明规范构造函数,如上所示。也可以通过省略形式参数列表来更紧凑地声明。在这样一个 紧凑的规范构造函数中,参数是隐式声明的,与record组件对应的私有字段不能在主体中分配,而是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;
    }
}

具有隐式声明的构造函数和方法的record类满足重要且直观的语义属性。例如,考虑如下声明的record类R:

record R(T1 c1, ..., Tn cn){ }

如果一个实例r1的R下列方式复制:

R r2 = new R(r1.c1(), r1.c2(), ..., r1.cn());

然后,假设r1不是空引用,则表达式的r1.equals(r2)总是被视为正确的计算结果。 显式声明的访问器和方法应该遵守这个不变量。但是,编译器通常不可能检查显式声明的方法是否遵守此不变量。

例如,下面的record类声明应该被认为是糟糕的风格,因为它的访问器方法“默默地”调整了record实例的状态,并且不满足上面的不变量:

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;
    }
}

此外,对于所有record类,隐式声明方法equals,以便它是自反的,并且它的行为与hashCode具有浮点组件的record类一致。同样,显式声明equals和 hashCode方法的行为应该相似。

record类规则

与普通类相比,record类的声明有许多限制:

record类声明没有extends子句。record类的超类总是java.lang.Record,类似于枚举类的超类总是java.lang.Enum。即使普通类可以显式扩展其隐式超类Object,record也不能显式扩展任何类,即使是其隐式超类Record。

  • record类是隐式的final,不能是abstract。这些限制强调了一个record类的 API 仅由其状态描述定义,并且不能在以后由另一个类增强。
  • 从record组件派生的字段是final. 此限制体现了广泛适用于数据载体类的默认不可变策略。
  • record类不能显式声明实例字段,也不能包含实例初始值设定项。这些限制确保record头单独定义record值的状态。
  • 自动派生的成员的任何显式声明必须与自动派生成员的类型完全匹配,而不管显式声明上的任何注解。任何访问器或equals或者hashCode方法的显式实现都应该小心保留record类的语义不变量。
  • record类不能声明native方法。如果record类可以声明一个native方法,那么record类的行为根据定义将取决于外部状态而不是record类的显式状态。没有具有本地方法的类可能是迁移到record的良好候选者。

除了上述限制之外,record类的行为类似于普通类:

  • record类的实例是使用new表达式创建的。
  • record类可以声明为顶级或嵌套的,并且可以是泛型的。
  • record类可以声明static方法、字段和初始值设定项。
  • record类可以声明实例方法。

record类可以实现接口。record类不能指定超类,因为这意味着继承的状态超出了标题中描述的状态。然而,record类可以自由地指定超接口并声明实例方法来实现它们。就像类一样,接口可以有效地表征许多record的行为。行为可以是域独立的(例如,Comparable)或域特定的,在这种情况下,record可以是捕获域的密封层次结构的一部分(见下文)。

  • record类可以声明嵌套类型,包括嵌套record类。如果record类本身是嵌套的,则它是隐式静态的;这避免了一个立即封闭的实例,它会默默地向record类添加状态。
  • record类及其标题中的组件可以使用注解进行修饰。根据注解的适用目标集,record组件上的任何注解都将传播到自动派生的字段、方法和构造函数参数。record组件类型上的类型注解也传播到自动派生成员中的相应类型使用。
  • record类的实例可以被序列化和反序列化。然而,该方法不能被提供定制writeObject,readObject,
    readObjectNoData,writeExternal,或readExternal的方法。record类的组件控制序列化,而record类的规范构造函数控制反序列化。

局部record类

生成和使用record类实例的程序可能会处理许多本身就是简单变量组的中间值。声明record类来为这些中间值建模通常会很方便。一种选择是声明静态和嵌套的“帮助程序”record类,就像今天许多程序声明帮助程序类一样。更方便的选择是在方法内声明一条record,靠近操作变量的代码。因此,我们定义了本地record类,类似于本地类的现有构造

在以下示例中,商家和每月销售额的聚合使用本地record类 建模MerchantSales。使用这个record类提高了以下流操作的可读性:

public class Temp {
    
    
    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());
    }
}

本地record类是嵌套record类的一种特殊情况。与嵌套record类一样,本地record类是隐式静态的。这意味着它们自己的方法不能访问封闭方法的任何变量;反过来,这避免了捕获一个立即封闭的实例,该实例会默默地向record类添加状态。本地record类是隐式静态的事实与本地类相反,本地类不是隐式静态的。事实上,局部类从来都不是静态的——隐式或显式——并且总是可以访问封闭方法中的变量。

本地枚举类和本地接口

添加本地record类是添加其他类型的隐式静态本地声明的机会。

嵌套的枚举类和嵌套的接口已经是隐式静态的,因此为了保持一致性,我们定义了本地枚举类和本地接口,它们也是隐式静态的。

内部类的静态成员

如果内部类声明显式或隐式静态成员,则 当前指定为编译时错误,除非该成员是常量变量。这意味着,例如,内部类不能声明record类成员,因为嵌套的record类是隐式静态的。

我们放宽此限制以允许内部类声明显式或隐式静态成员。特别是,这允许内部类声明作为record类的静态成员。

record组件上的注解

record组件在record声明中具有多个角色。record组件是一等概念,但是每个组件也对应一个同名同类型的字段,同名同返回类型的访问器方法,同名同类型的规范构造函数的形参.

这就提出了一个问题:当一个组件被注解时,实际上注解的是什么?答案是,“此特定注解适用的所有元素。” 这使得在其字段、构造函数参数或访问器方法上使用注解的类能够迁移到record,而无需冗余声明这些成员。例如,像下面这样的类

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;
    }
} 

可以迁移到等效的、更具可读性的record声明:

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,它适用于字段声明和方法声明。

回到record组件上的注解,这些注解出现在它们适用的相应程序点处。换句话说,传播由使用@Target 元注解的开发人员控制。传播规则是系统和直观的,所有适用的都遵循:

  • 如果record组件上的注解适用于字段声明,则该注解出现在相应的私有字段上。
  • 如果record组件上的注解适用于方法声明,则该注解出现在相应的访问器方法上。
  • 如果record组件上的注解适用于形参,则如果未显式声明,则注解出现在规范构造函数的相应形参上,否则出现在紧凑构造函数的相应形参上,如果显式声明.
  • 如果record组件上的注解适用于一种类型,则该注解将传播到以下所有内容:
    • 对应字段的类型
    • 对应访问器方法的返回类型
    • 规范构造函数对应的形参类型
    • record组件的类型(可在运行时通过反射访问)

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

record组件上的声明注解不会在运行时通过反射 API与record组件相关联的那些 注解中,除非注解是元注解的 @Target(RECORD_COMPONENT)。

兼容性和迁移

抽象类java.lang.Record是所有record类的公共超类。无论您启用还是禁用预览功能java.lang.Record,每个 Java
源文件都会隐式导入该类以及java.lang包中的所有其他类型。但是,如果您的应用程序导入Record从不同包命名的另一个类,您可能会收到编译器错误。

考虑以下 的类声明com.myapp.Record:

package com.myapp;

public class Record {
    
    
    public String greeting;

    public Record(String greeting) {
    
    
        this.greeting = greeting;
    }
}

以下示例 ,使用通配符org.example.MyappPackageExample导入 com.myapp.Record但不编译:

package org.example;

import com.myapp.*;

public class MyappPackageExample {
    
    
    public static void main(String[] args) {
    
    
        Record r = new Record("Hello world!");
    }
}

编译器会生成类似于以下内容的错误消息:

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous Record r = new Record("Hello world!");
^ both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous Record r = new Record("Hello world!");
^ both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

无论Record在com.myapp包,并Record在java.lang包装均采用进口带通配符。因此,两个类都不优先,编译器在遇到使用简单名称时会生成错误消息Record。

要使此示例能够编译,import可以更改语句以导入 的完全限定名称Record:

import com.myapp.Record;

在java.lang包中引入类很少见,但有时是必要的。之前的示例Enum在 Java 5、ModuleJava 9 和RecordJava 14 中。

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:
{ConstructorModifier} SimpleTypeName ConstructorBody

类文件表示

Record的class文件使用Record属性来存储有关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];
}

如果record组件具有不同于已擦除描述符的泛型签名,则结构中必须有一个Signature属性 record_component_info。

反射API

我们将两个公共方法添加到java.lang.Class:

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

  • boolean isRecord()— 如果给定的类被声明为record,则返回 true。(比较isEnum。)

备选方案

record类可以被认为是元组的名义形式。我们可以实现结构元组,而不是record类。然而,虽然元组可能提供一种轻量级的方式来表达一些聚合,但结果通常是劣质聚合:

  • Java 设计理念的一个核心方面是名称很重要。类及其成员具有有意义的名称,而元组和元组组件则没有。也就是说,Personrecord类成分firstName和lastName比两个字符串的匿名元组更清晰,更安全。
  • 类允许通过其构造函数进行状态验证;元组通常不会。某些数据聚合(例如数字范围)具有不变量,如果由构造函数强制执行,则此后可以依赖这些不变量。元组不提供这种能力。
  • 类可以具有基于其状态的行为;将状态和行为放在一起使行为更容易发现和更容易访问。作为原始数据的元组没有提供这样的功能。

依赖关系

record类与当前处于预览状态的另一个功能配合良好,即密封类( JEP 360 )。例如,可以显式声明一系列record类来实现相同的密封接口:

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 {} 

record类和密封类的组合有时称为 (algebraic)代数数据类型。record类允许我们表达 products,密封类允许我们表达 sum。

除了record类和密封类的组合之外,record类也很自然地适用于模式匹配。因为record类将它们的 API
与其状态描述耦合,我们最终也将能够为record类推导出解构模式,并使用密封类型信息来确定switch具有类型模式或解构模式的表达式的穷举性。

猜你喜欢

转载自blog.csdn.net/sayWhat_sayHello/article/details/121902216