编程

Java 中的 record 关键字

33 2024-09-18 02:18:00

1. 介绍

在许多 Java 应用中,在对象之间传递不可变数据是最常见但最平凡的任务之一。

在 Java 14 之前,这需要创建一个包含样板字段和方法的类,这些字段和方法容易出现琐碎的错误和混乱的意图。

随着 Java 14 的发布,现在我们可以使用 record 来解决这些问题。

本文中,我们将研究 record 的基本原理,包括它们的意图、生成方法和自定义技术。

2. 意图

通常,我们编写类只是为了持有数据,如数据库结果、查询结果或来自服务的信息。

在许多情况下,这些数据是不可变的,因为不可变性确保了数据的有效性,而无需同步

为了实现这一点,我们使用以下步骤创建数据类:

  1. private, final 字段用于每个数据
  2. getter 用于每个字段
  3. public 构造函数,每个字段都有相应的参数
  4. equals 方法,当所有字段都匹配时,该方法对同一类的对象返回 true
  5. hashCode 方法,当所有字段都匹配时,返回同一个的值
  6. toString 方法,其中包含类名以及每个字段的名称以及其对应的值

比如,我们可以创建一个带有名字和地址的简单 Person 数据类:

public class Person {

    private final String name;
    private final String address;

    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Person)) {
            return false;
        } else {
            Person other = (Person) obj;
            return Objects.equals(name, other.name)
              && Objects.equals(address, other.address);
        }
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", address=" + address + "]";
    }

    // standard getters
}

虽然这样实现了我们的目标,但有两个问题:

  1. 有太多的模板代码
  2. 我们模糊了类的目的:用名字和地址代表一个人

第一种情况,我们必须对每个数据类重复同样乏味的过程,为每条数据单调地创建一个新字段;创建 equalshashCodetoString 方法;并创建一个接受每个字段的构造函数。

虽然 IDE 可以自动生成许多这样的类,但当我们添加新字段时,它们无法自动更新我们的类。例如,如果我们添加一个新字段,我们必须更新equals 方法来合并这个字段。

在第二种情况下,额外的代码掩盖了我们的类只是一个有两个 String 字段(nameaddress)的数据类。

更好的方法是明确声明我们的类是一个数据类。

3. 基础

从 JDK 14 开始,我们可以用 record 替换重复的数据类。record 是不可变的数据类,只需要字段的类型和名称。

equalshashCodetoString 方法,以及 privatefinal 字段和 public 构造函数,都是由 Java 编译器生成的。

要生成一条 Person 记录,我们可以使用  record 关键字:

public record Person (String name, String address) {}

3.1. 构造函数

使用记录(record),它为我们生成了一个公共构造函数,每个字段都有一个参数。

以 Person 记录为例,等效构造函数为:

public Person(String name, String address) {
    this.name = name;
    this.address = address;
}

此构造函数的使用方式与类相同,可以从记录中实例化对象:

Person person = new Person("John Doe", "100 Linda Ln.");

3.2. Getter

我们也接收了公共 getter 方法,其名称与我们的字段名称匹配。

以 Person 记录为例,这意味着它有一个 name() 和一个 address() getter:

@Test
public void givenValidNameAndAddress_whenGetNameAndAddress_thenExpectedValuesReturned() {
    String name = "John Doe";
    String address = "100 Linda Ln.";

    Person person = new Person(name, address);

    assertEquals(name, person.name());
    assertEquals(address, person.address());
}

3.3. equals

此外,它还为我们生成了一个 equals 方法。

如果提供的对象属于同一类型,并且其所有字段的值都匹配,则此方法返回 true

@Test
public void givenSameNameAndAddress_whenEquals_thenPersonsEqual() {
    String name = "John Doe";
    String address = "100 Linda Ln.";

    Person person1 = new Person(name, address);
    Person person2 = new Person(name, address);

    assertTrue(person1.equals(person2));
}

如果两个 Person 实例之间的任何字段有不同,equals 将返回 false

3.4. hashCode

类似于 equals 方法,它也为我们生成了对应的  hashCode 方法。

如果两个 Person 对象的所有字段值都匹配,我们的 hashCode 方法将返回相同的值(除非由于生日悖论而发生冲突)

@Test
public void givenSameNameAndAddress_whenHashCode_thenPersonsEqual() {
    String name = "John Doe";
    String address = "100 Linda Ln.";

    Person person1 = new Person(name, address);
    Person person2 = new Person(name, address);

    assertEquals(person1.hashCode(), person2.hashCode());
}

如果任何字段值有不同,hasCode 值大概率会不一样。不过,hashCode() 合约不完全保证。

3.5. toString

最后,我们还收到一个 toString 方法,该方法产生一个字符串,其中包含记录的名称,随后在方括号中跟每个字段的名称及其对应值。

因此,实例名字为 "John Doe“ 以及地址为 ”100 Linda Ln.“ 的 Person 将产生以下 toString 结果:

Person[name=John Doe, address=100 Linda Ln.]

4. 构造函数

虽然它也生成了 public 的构造函数,我们仍然可以自定义我们的构造函数实现。

这种自定义旨在用于验证,应尽可能简单。

比如,我们使用如下的构造函数实现,来确保提供给 Person 记录的 nameaddress 不是 null

public record Person(String name, String address) {
    public Person {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
}

我们也通过提供不同的参数列表来带有创建不同参数的构造函数:

public record Person(String name, String address) {
    public Person(String name) {
        this(name, "Unknown");
    }
}

与类构造函数一样,可以使用 this 关键字(例如 this.namethis.address)引用字段,并且参数与字段的名称(即 nameaddress)匹配。

请注意,使用与生成的公共构造函数相同的参数创建构造函数是有效的,但这需要手动初始化每个字段

public record Person(String name, String address) {
    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

此外,声明一个紧凑构造函数和一个具有与生成的构造函数匹配的参数列表的构造函数会导致编译错误

因此,以下内容无法编译:

public record Person(String name, String address) {
    public Person {
        Objects.requireNonNull(name);
        Objects.requireNonNull(address);
    }
    
    public Person(String name, String address) {
        this.name = name;
        this.address = address;
    }
}

5. 静态变量 & 方法

与常规 Java 类一样,我们也可以在记录中包含静态变量和方法。

我们使用与类相同的语法声明静态变量:

public record Person(String name, String address) {
    public static String UNKNOWN_ADDRESS = "Unknown";
}

同样,我们使用与类相同的语法声明静态方法:

public record Person(String name, String address) {
    public static Person unnamed(String address) {
        return new Person("Unnamed", address);
    }
}

然后,我们可以使用记录(记录)的名称引用静态变量和静态方法:

Person.UNKNOWN_ADDRESS
Person.unnamed("100 Linda Ln.");

6. 结论

本文中,我们研究了 Java 14 中引入的 record 关键字,包括它的基本概念和复杂的细节。

使用带有编译器生成方法的记录(record),我们可以减少样板代码,提高不可变类的可靠性。