1. record 类与不可变性
不可变性,指的是对象的一种属性:其状态在创建之后不能被修改。换句话说:对象一旦创建,你就不能改变它的内部数据。就这么简单!
想象一张火车票。一旦打印出来,你就不能再更改发车日期或出发地点(当然,不考虑用图片软件做手脚的情况)。车票就是一个不可变对象。如果你需要另一张票——那就买一张新的。
在编程中,这类对象被称为不可变对象(immutable objects)。它们能保护程序免受意外修改,使代码更安全、更可预测。
不可变对象的特征
- 对象的所有字段都是 final(只能赋值一次,通常在构造器中)。
- 没有 setter(用于更改字段值的方法)。
- 所有返回内部数据的方法要么返回副本,要么这些数据本身也是不可变的。
在 Java 中,record 类的设计初衷就是更简单、无痛地创建不可变的数据结构。
为什么 record 是不可变的?
- record 的所有组件都是 final。
当你声明一个 record 时,编译器会自动将其所有字段设为 private final。这意味着对象创建后,你无法再修改其字段。 - 没有 setter。
在 record 类中不能添加 setX(int x) 这样的方式——编译器不允许你在对象创建后修改字段值。 - 构造器只赋值一次。
所有值都只在创建对象的那一刻被设置。
示例
public record Point(int x, int y) {}
Point p = new Point(5, 10);
// p.x = 7; // 编译错误:字段 x 为 private 且是 final
// p.x(7); // 错误:没有 setter!
System.out.println(p.x()); // 5
尝试修改字段或调用不存在的 setter 会导致编译错误。Java 严格遵守不可变性的契约。
2. 不可变对象的优势
多线程安全
在多线程程序中(如今这类程序占多数!),不可变对象就像防弹衣。如果对象不能被修改,不同线程就可以放心读取它,而不必担心有人在此刻更改数据。无需同步访问,也不用为数据竞争而焦虑。
事实:标准 Java 库中许多在多线程场景下常用的类,要么是不可变的,要么被专门设计为防止修改。
简化代码理解
如果对象是不可变的,你总能确定:把它传给其他方法或类——它不会在“背后”被改动。这大大简化了代码的阅读与调试。不用猜测是谁在何处改变了字段——没人能改!
适合作为集合中的键
不可变对象非常适合在诸如 HashMap 或 HashSet 这类集合中用作键。为什么?因为它们的 equals 与 hashCode 只依赖于不会变化的字段。也就是说,对象不会因为状态变化而在集合中“丢失”。
更少隐藏 bug
可变对象很容易因为误传引用而被意外破坏。不可变对象就像一本印好的书:没人能随意撕页或改写内容。
3. 与普通类的比较
我们来比较一下普通类与 record 类的行为。以平面上的点为例。
普通类(mutable)
public class PointClass {
private int x;
private int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
}
我们可以先创建对象,然后任意多次修改它的状态:
PointClass p = new PointClass(1, 2);
p.setX(10); // 现在 p.x 为 10
record 类(immutable)
public record Point(int x, int y) {}
创建对象之后——它会永远保持创建时的样子:
Point p = new Point(1, 2);
// p.x = 10; // 错误!无法访问字段
// p.x(10); // 错误!没有 setter
表:行为对比
| 普通类 | record 类 | |
|---|---|---|
| 字段 | 任意 | 仅 private final |
| Setter | 可以添加 | 不能添加 |
| 可变性 | 可变(mutable) | 不可变 |
| 自动生成 | 否 | 是(equals、hashCode、toString) |
4. 实践:如何利用 record 类的不可变性
我们来在一个小例子中使用 record 类。假设有一个银行应用,需要保存交易信息:
public record Transaction(String fromAccount, String toAccount, double amount) {}
创建对象:
Transaction t = new Transaction("12345", "67890", 1500.0);
System.out.println(t);
// Transaction[fromAccount=12345, toAccount=67890, amount=1500.0]
尝试把钱“转到”另一个账户:
// t.toAccount = "11111"; // 错误!字段为 final,无法访问
// t.toAccount("11111"); // 错误!没有 setter
如果需要另一笔交易——创建一个新对象:
Transaction t2 = new Transaction(t.fromAccount(), "11111", t.amount());
重要:不可变并不意味着“不方便”。这只是另一种工作方式:当需要新状态时——就创建一个新对象。
5. 不可变性的注意事项:需要牢记什么
不可变性并非绝对!
record 类保证其字段不会改变。但如果字段是对可变对象的引用(例如数组或普通类),那么该对象的内容仍然可能被修改。
数组示例
public record DataHolder(int[] data) {}
int[] arr = {1, 2, 3};
DataHolder holder = new DataHolder(arr);
arr[0] = 99;
System.out.println(holder.data()[0]); // 99!数组被修改了
结论:如果你想要真正的不可变性,请只使用不可变类型(String、Integer、其他 record 等),或者在 record 类的规范构造器中对可变对象进行防御性拷贝。例如:
int[] copy = Arrays.copyOf(data, data.length);
6. 如何把普通类做成不可变
如果你想让一个普通类变为不可变(immutable),需要手动做到:
- 将所有字段标记为 private final,
- 不添加 setter,
- 所有字段只通过构造器初始化,
- 如果字段是可变对象,进行防御性拷贝(defensive copy)。
示例
public final class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
你会发现,用 record 类实现会更简单、更简洁:
public record User(String name, int age) {}
7. 使用不可变 record 类的常见错误
错误 1:在创建后尝试修改字段。
新手常会对 record 对象写出 p.x = 42; 或 p.x(42);。但编译器会立刻提示:“不可以!字段是 final,且没有 setter”。
错误 2:将可变对象用作 record 的组件。
如果你在 record 中加入了 List、Map、数组或其他可变对象类型的字段,那么 record 本身并不能阻止这些对象内容被修改。比如,如果有一个 record User(List<String> hobbies),别人就可能向列表中添加或删除元素,从而改变你的 record 对象的状态。为避免这种情况,使用不可变集合(List.copyOf、Collections.unmodifiableList),或在 record 的构造器中对集合进行拷贝。
错误 3:误解不可变性的含义。
有人以为对象是 record 就能抵御一切修改。事实上,如果字段是指向可变对象的引用,其内容仍可被更改,这会导致意想不到的 bug。
GO TO FULL VERSION