记录类(record)
本教程共 100 篇 · 第 49 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
49. 记录类(record)
本节目标:学会用
record声明不可变的数据载体类,搞懂编译器自动帮你生成了哪些方法,并掌握用紧凑构造方法做参数校验。
又是一堆重复代码
假设你要表示一个「二维坐标点」,类里有两个字段 x、y,外加它们的取值方法、equals、hashCode、toString,还得有个构造方法。手写下来要好几十行,而且全是样板,没有任何业务逻辑。
如果项目里有几十个这样的「数据类」(坐标、金额、配置项……),代码量会爆炸。
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]
Noterecord 的字段是
final的,所以对象创建后不可修改——这就是「不可变对象」。它天生线程安全,也很适合当 Map 的 key。访问器名字就是组件名本身(如x()),不要习惯性地写getX()。
紧凑构造方法做校验
括号里列出的组件会参与 equals、hashCode、toString、构造方法和访问器。如果想在创建时加校验(比如半径不能为负),可以用紧凑构造方法(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(),这段代码既安全又干净。
Noterecord 实现接口是允许的(如上面
implements Shape)。注意 record 默认不是final子类——它自己是final的,但可以实现接口。上面Circle、Rect作为 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 一样,都是用来表达「有限、固定取值」的语法糖。