CodeGym /课程 /JAVA 25 SELF /不可变性——record 类的不可变性

不可变性——record 类的不可变性

JAVA 25 SELF
第 22 级 , 课程 1
可用

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 库中许多在多线程场景下常用的类,要么是不可变的,要么被专门设计为防止修改。

简化代码理解

如果对象是不可变的,你总能确定:把它传给其他方法或类——它不会在“背后”被改动。这大大简化了代码的阅读与调试。不用猜测是谁在何处改变了字段——没人能改!

适合作为集合中的键

不可变对象非常适合在诸如 HashMapHashSet 这类集合中用作键。为什么?因为它们的 equalshashCode 只依赖于不会变化的字段。也就是说,对象不会因为状态变化而在集合中“丢失”。

更少隐藏 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) 不可变
自动生成 是(equalshashCodetoString

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!数组被修改了

结论:如果你想要真正的不可变性,请只使用不可变类型(StringInteger、其他 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 中加入了 ListMap、数组或其他可变对象类型的字段,那么 record 本身并不能阻止这些对象内容被修改。比如,如果有一个 record User(List<String> hobbies),别人就可能向列表中添加或删除元素,从而改变你的 record 对象的状态。为避免这种情况,使用不可变集合(List.copyOfCollections.unmodifiableList),或在 record 的构造器中对集合进行拷贝。

错误 3:误解不可变性的含义。
有人以为对象是 record 就能抵御一切修改。事实上,如果字段是指向可变对象的引用,其内容仍可被更改,这会导致意想不到的 bug。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION