首页 / Java 入门教程 / 记录类(record)

Java 入门教程

记录类(record)

本教程共 100 篇 · 第 49 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程记录类record不可变Java16

49. 记录类(record)

本节目标:学会用 record 声明不可变的数据载体类,搞懂编译器自动帮你生成了哪些方法,并掌握用紧凑构造方法做参数校验。

又是一堆重复代码

假设你要表示一个「二维坐标点」,类里有两个字段 xy,外加它们的取值方法、equalshashCodetoString,还得有个构造方法。手写下来要好几十行,而且全是样板,没有任何业务逻辑。

如果项目里有几十个这样的「数据类」(坐标、金额、配置项……),代码量会爆炸。

Java 16 正式引入的 record(记录类) 就是专门消灭这种重复的。(它在 Java 14、15 是预览特性,Java 16 转正,对应 JEP 395。)

一行声明一个数据类

class 换成 record,在名字后面的括号里列出「组件(components)」即可:

public record Point(double x, double y) { }
```java

就这一行,编译器**自动**为你生成了:

- 两个 `private final` 字段 `x`、`y`;
- 一个全参构造方法 `Point(double x, double y)`;
- 两个访问器 `x()`、`y()`(注意:是 `x()` 而不是 `getX()`);
- 正确的 `equals()`、`hashCode()`、`toString()`。

立刻能用:

```java
Point p = new Point(3, 4);
System.out.println(p.x());        // 3
System.out.println(p);            // Point[x=3, y=4]
Note

record 的字段是 final 的,所以对象创建后不可修改——这就是「不可变对象」。它天生线程安全,也很适合当 Map 的 key。访问器名字就是组件名本身(如 x()),不要习惯性地写 getX()

紧凑构造方法做校验

括号里列出的组件会参与 equalshashCodetoString、构造方法和访问器。如果想在创建时加校验(比如半径不能为负),可以用紧凑构造方法(Compact Constructor)

public record Circle(double radius) {
    public Circle {
        if (radius < 0) {
            throw new IllegalArgumentException("半径不能为负");
        }
    }
}
```bash

紧凑构造方法**没有参数列表**——它的参数就是记录组件本身,里面直接写校验逻辑即可。校验在自动生成的字段赋值之前执行,所以能在对象还没「成型」时拦住非法值。

> [!TIP]
> 想给访问器「加工」返回结果,也可以单独重写它:比如 `public double radius() { return Math.round(radius * 100.0) / 100.0; }`,覆盖掉自动生成的版本。

## record 的边界规则

record 有一些和普通类不同的硬规矩,提前记牢:

- **隐式 final**:record 不能被别的类继承,也不能去 `extends` 另一个类(它隐式继承自 `java.lang.Record`)。
- **不能是 abstract**:record 必须能被实例化,所以不能是抽象类。
- 组件字段都是 `final`,没有 setter。

> [!WARNING]
> 不要为了「能改字段」而给 record 加 setter。如果数据需要变化,说明它不该用 record,老老实实写普通 `class`。record 的初心就是「不可变的值」。

## 可以加静态成员和额外方法

record 虽然简洁,但不是只能当哑巴数据。你可以加静态字段、静态方法,以及普通实例方法:

```java
public record Point(double x, double y) {
    // 静态常量
    public static final Point ORIGIN = new Point(0, 0);

    // 实例方法(行为)
    public double distanceTo(Point other) {
        double dx = x - other.x;
        double dy = y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}
Point p = new Point(3, 4);
System.out.println(p.distanceTo(Point.ORIGIN));   // 5.0
```bash

只是**不能**额外加「实例字段」——所有实例状态都必须来自声明的组件,这是 record 保持不可变和语义清晰的代价。

## 与密封类、模式匹配是好搭档

record 经常和密封类、模式匹配一起出现。比如密封的「形状」家族,成员用 record 表示特别自然:

```java
sealed interface Shape permits Circle, Rect { }
record Circle(double r) implements Shape { }
record Rect(double w, double h) implements Shape { }

double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Rect r   -> r.w() * r.h();
    };
}

因为 Shape 被密封、分支穷举,加上 record 组件访问器 c.r(),这段代码既安全又干净。

Note

record 实现接口是允许的(如上面 implements Shape)。注意 record 默认不是 final 子类——它自己是 final 的,但可以实现接口。上面 CircleRect 作为 record 本就不可被继承,配合密封接口的 permits 非常契合。

record 模式:匹配的同时拆包

Java 21 转正的 record 模式(Record Pattern,JEP 440) 更进一步:判断类型的同时,直接把组件「拆」成局部变量,省掉逐个调访问器的动作。

record Point(int x, int y) { }

static String describe(Object o) {
    return switch (o) {
        case Point(int x, int y) when x == y -> "在对角线上:" + x;
        case Point(int x, int y)             -> "坐标 (" + x + ", " + y + ")";
        default                              -> "不是点";
    };
}
```java

`case Point(int x, int y)` 一行干了两件事:确认对象是 `Point`,并把两个组件解构出来。比先 `instanceof` 再写 `p.x()`、`p.y()` 干净得多。

嵌套的 record 还能一层层往里拆:

```java
record Line(Point start, Point end) { }

if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    int dx = x2 - x1, dy = y2 - y1;
    System.out.println("长度平方:" + (dx * dx + dy * dy));
}
Tip

解构时组件类型必须写准。写成 case Point(long x, long y) 编译不过——record 模式按声明的组件类型精确匹配,不会自动放宽成更大的类型。

完整可运行示例

public record Point(double x, double y) {
    public Point {
        // 紧凑构造方法:校验
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("坐标不能为负");
        }
    }

    public double distanceTo(Point other) {
        double dx = x - other.x;
        double dy = y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

public class Main {
    public static void main(String[] args) {
        Point a = new Point(0, 0);
        Point b = new Point(3, 4);
        System.out.println(a.distanceTo(b));   // 5.0
        System.out.println(a.equals(new Point(0, 0)));  // true
    }
}
```bash

## 什么时候不该用 record

record 不是银弹。下面这些情况,老老实实写普通 `class`:

- 对象需要**可变**:record 字段全是 `final`,天然不可变,需要 setter 就别用;
- 想继承或被继承:record 隐式 `final`,不能当父类,也不能继承别的类;
- 有大量「和行为无关的额外状态」:record 的所有实例状态都来自组件,额外加不了实例字段。

反过来,只要是「一组值,创建后不修改,主要用来携带和传递数据」(比如方法返回值、配置项、Map 的 key、JSON 映射对象),record 就是首选。

## record 与 Lombok 的关系

有些人用 Lombok 的 `@Data`、`@Value` 来少写代码,效果看起来和 record 类似。但 record 是**语言层面**支持的,不依赖注解处理器,生成的 `equals`/`hashCode` 语义更明确,编译更快、调试更直观。在 Java 16+ 的项目里,纯数据载体优先用 record,而不是 Lombok。此外,record 自动生成的 `toString` 会打印组件名和值,调试时一眼就能看清对象内容,这也是它受青睐的原因之一。

## 常见疑问

**能不能给 record 加一个实例字段?**

不能。硬写会当场编译失败:

```text
error: field declaration must be static

record 的所有实例状态只能来自括号里的组件,这是它保证「值语义」的前提。想缓存计算结果,要么用静态字段,要么每次现算。

紧凑构造方法里能改参数的值吗?

能改,改完的值会被写进字段,常用来做「归一化」:

public record Name(String value) {
    public Name {
        value = value == null ? "" : value.trim();   // 修正后才落到字段
    }
}
```java

这里赋给 `value` 的是参数变量,编译器在方法体结束后才把它赋给同名字段,所以修正生效。

**继承一个 record 会怎样?**

直接报错,因为 record 隐式 `final`:

```text
error: cannot inherit from final Point

需要「一族相关类型」时,用密封接口 + 多个 record 实现,而不是继承。

小结

  • record 是 Java 16 转正(JEP 395)的不可变数据载体,用一行声明替代大量样板代码。
  • 编译器自动生成 final 字段、全参构造、x() 形式访问器、equals/hashCode/toString
  • 用紧凑构造方法(无参数列表)做参数校验;可自定义访问器和实例方法。
  • record 隐式 final、不能继承也不能被继承,不能加实例字段。
  • record 适合表示「值」,常与密封类、模式匹配搭配,写出简洁且安全的代码。

下一章讲枚举(enum),它和 record 一样,都是用来表达「有限、固定取值」的语法糖。