Java中使用Record创建不可变对象的简明指南

本文详细介绍了Java 16中引入的Record特性,如何用它简化不可变对象的创建,包括自动生成构造方法、getter和常用方法,减少模板代码,并对比了传统方式的实现差异。

不可变对象在Java中使用Record的实现

创建后内容不变的对象通常很有用。要了解如何构建此类类的完整说明,您可以阅读我之前的文章《Java中的不可变对象》。

假设我们想构建一个具有两个字段的PersonClass:firstName和lastName。要创建不可变实例,此类必须:

  • 有一个构造函数来初始化这些字段
  • 将字段保持为私有和最终,以确保在构造函数中设置后无法更改
  • 提供getter方法来访问这些字段
  • 不可扩展,因此我们将其标记为final
  • 重写equals、hashCode和toString方法,考虑所有字段

如果我们在Java 16之前构建这样的类,最终会得到类似以下40行代码片段的内容…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.davidemarino;

import java.util.Objects;

public final class PersonClass {
    private final String firstName;
    private final String lastName;

    public PersonClass(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName);
    }

    @Override
    public String toString() {
        return "PersonClass{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

…我们可以如下使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.davidemarino;

public class Main {
    public static void main(String[] args) {
        PersonClass personClass = new PersonClass("John", "Doe");
        System.out.println("First name: " + personClass.getFirstName());
        System.out.println("Last name: " + personClass.getLastName());
        System.out.println(personClass);
    }
}

执行它得到以下输出:

1
2
3
First name: John
Last name: Doe
PersonClass{firstName='John', lastName='Doe'}

创建不可变类所需的大部分代码都是样板代码。但我们真正需要知道什么?只需要类名和其字段。

自Java 16起,您可以使用record关键字来定义这样的类。 Record本质上是创建以下内容的快捷方式:

  • 一个final类
  • 具有私有final字段
  • getter方法
  • 一个设置所有字段的构造函数
  • 重写的equals、hashCode和toString方法

要定义record,请使用record关键字(而不是final class)并在头中声明字段,如下所示:

1
2
3
package com.davidemarino;

public record PersonRecord(String firstName, String lastName) { }

我们可以如下使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.davidemarino;

public class Main {
    public static void main(String[] args) {
        PersonClass personClass = new PersonClass("John", "Doe");
        System.out.println("First name: " + personClass.getFirstName());
        System.out.println("Last name: " + personClass.getLastName());
        System.out.println(personClass);

        PersonRecord personRecord = new PersonRecord("John", "Doe");
        System.out.println("First name: " + personRecord.firstName());
        System.out.println("Last name: " + personRecord.lastName());
        System.out.println(personRecord);
    }
}

如您所见,有一个关键区别:getter方法遵循不同的命名约定。不是getFirstName()和getLastName(),而是firstName()和lastName()。 然而,其余代码保持相同。

但是,如果我们有可变对象字段而不是字符串会发生什么? 在传统方法中,我们需要在构造函数和getter方法中都创建可变字段的副本。 例如,假设我们想向PersonClass添加一个List preferences字段。代码应更新如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.davidemarino;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class PersonClass {
    private final String firstName;
    private final String lastName;
    private final List<String> preferences;
    
    public PersonClass(String firstName, String lastName, List<String> preferences) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.preferences = new ArrayList<>(preferences);
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public List<String> getPreferences() {
        return new ArrayList<>(preferences);
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(preferences, that.preferences);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, preferences);
    }

    @Override
    public String toString() {
        return "PersonClass{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", preferences=" + preferences +
                '}';
    }
}

我们可以使用record完成类似的操作,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.davidemarino;

import java.util.ArrayList;
import java.util.List;

public record PersonRecord(String firstName, String lastName, List<String> preferences) {
    public PersonRecord {
        preferences = new ArrayList<>(preferences);
    }

    public List<String> preferences() {
        return new ArrayList<>(preferences);
    }
}

这部分代码称为紧凑规范构造函数…

1
2
3
public PersonRecord {
    preferences = new ArrayList<>(preferences);
}

…等效于以下标准构造函数。

1
2
3
4
5
6
public PersonRecord(String firstName, String lastName, List<String> preferences) {
    preferences = new ArrayList<>(preferences);
    this.firstName = firstName;
    this.lastName = lastName;
    this.preferences = preferences;
}

因此,要创建不可变实例的类,您可以使用record结构,节省大量样板代码。

结论

在Java中传统创建不可变对象需要大量样板代码。随着Java 16中record的引入,开发人员现在有一种简洁且表达性强的方式来定义不可变数据结构。Record自动生成构造函数、封装字段并重写equals、hashCode和toString等方法,显著减少了手动编码工作。这使得代码更易于阅读、维护且更不容易出错。总体而言,record是对Java类型系统的一个强大补充,简化了健壮的不可变数据模型的创建。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计