不可变对象在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类型系统的一个强大补充,简化了健壮的不可变数据模型的创建。