还在为每个 DTO、VO 写一堆 private final 字段、getter、equals、hashCode 和 toString 吗?Java Record 与模式匹配是 JDK 16 正式引入、在 JDK 21 全面定稿的语言特性,它们把”数据载体类”和”类型判别”这两件最日常的事变得既简洁又类型安全。本文从样板代码的痛点出发,用一组可运行示例讲清 Record 的不可变模型、模式 instanceof、模式 switch,以及两者组合后威力最大的”代数数据类型”写法,并给出接入 Spring 控制器与 Lombok 迁移的实战建议。
一、为什么需要 Record:POJO 的样板代码之痛
一个只承载数据的坐标点,用传统类写出来常常是这个样子。真正的业务逻辑还没开始,先写了一屏幕”基础设施”代码:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) {
return o instanceof Point p && p.x == x && p.y == y;
}
@Override public int hashCode() {
return java.util.Objects.hash(x, y);
}
@Override public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
同样是坐标点,用 Record 只需一行,且自动获得不可变性、正确的 equals/hashCode/toString 以及组件访问器:
public record Point(int x, int y) {}
二、Record 基础语法与不可变性
2.1 声明与默认成员
record Point(int x, int y) {} 中的 x、y 是组件(component)。编译器会自动生成:私有 final 字段、带参构造器、同名访问器(x()、y(),注意不是 getX())、以及基于全部组件的 equals/hashCode/toString。Record 隐式 final,且不能继承其他类(但可以实现接口)。
2.2 自定义访问器与方法
可以为组件加紧凑构造器做校验,或重写访问器。下面的例子在构造时保证半径非负,并把访问器改为只读归一化结果:
public record Circle(double radius) {
public Circle {
if (radius < 0) throw new IllegalArgumentException("radius < 0");
}
public double area() {
return Math.PI * radius * radius;
}
}
传统类与 Record 的取舍可以一句话概括:只为传数据、不需要可变状态时就用 Record。下面这张表列出了关键差异。
| 维度 | 传统 class | Record |
|---|---|---|
| 样板代码 | 需手写 getter/equals 等 | 编译器自动生成 |
| 可变性 | 可自由定义可变字段 | 组件默认 final 不可变 |
| 继承 | 可继承类/实现接口 | 不能继承类,可实现接口 |
| 适用场景 | 有行为/状态的领域对象 | 纯数据载体、DTO、返回值 |
2.3 Record 与 Lombok 的取舍
很多团队习惯用 Lombok 的 @Value/@Data 削减样板。Record 与 Lombok @Value 都能产出不可变类,但底层思路不同:Lombok 是编译期注解处理器生成源码(细节可回顾 Java 注解处理器实战),而 Record 是语言原生特性,语义更明确、IDE 与静态分析工具支持更稳,也不会引入额外的构建依赖。经验法则:新项目且 JDK≥16,优先用 Record;存量项目或需要大量自定义行为(如 JPA 可写实体、复杂业务方法)时,Lombok 仍有一席之地。
三、模式匹配:instanceof 与 switch 的范式升级
3.1 模式 instanceof:告别先判型再强转
过去的写法要先 instanceof 再强制转型并声明新变量;模式 instanceof 让变量在判断成功的同时直接绑定类型,作用域也自动限定在安全区间内:
Object obj = "hello";
// 旧写法
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// 模式 instanceof(JDK 16+)
if (obj instanceof String s) {
System.out.println(s.length());
}
3.2 模式 switch(JDK 21 定稿)
类型模式进入 switch 后,多分支类型判别变得线性且穷尽(配合 sealed 接口时编译器还能检查是否覆盖全部子类型):
static String describe(Number n) {
return switch (n) {
case Integer i -> "int " + i;
case Double d -> "double " + d;
case null -> "null";
default -> "other: " + n;
};
}
3.3 守卫模式:when 子句细化分支
JDK 21 的模式 switch 还支持 when 守卫,在同一个类型分支内再做条件细化,而不必额外嵌套 if。它让"按范围/状态分流"的写法保持线性:
static String tier(int score) {
return switch (score) {
case Integer i when i >= 90 -> "A";
case Integer i when i >= 75 -> "B";
case Integer i when i >= 60 -> "C";
default -> "D";
};
}
四、Record + 模式匹配的组合实战:表达式求值器
把 sealed interface + Record 子类型 + 模式 switch 组合起来,就能用极少的代码表达"代数数据类型(ADT)"。下面是一棵最小化的算术表达式树及其求值:
sealed interface Expr permits Const, Add, Mul {}
record Const(int v) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
static int eval(Expr e) {
return switch (e) {
case Const c -> c.v();
case Add a -> eval(a.left()) + eval(a.right());
case Mul m -> eval(m.left()) * eval(m.right());
}; // 编译器保证覆盖所有子类型,无需 default
}
// 用法:eval(new Mul(new Const(2), new Add(new Const(3), new Const(4)))) == 14
这种写法把"数据形状"和"对形状的算法"干净地分离开:新增一种表达式只需新增一个 Record,switch 会在编译期提醒你补充分支,避免了散落在各处的 if/else 与强制转型。
五、常见坑与最佳实践
5.1 不要把 Record 当可变容器
Record 的语义是"值相等即相同"。如果你的组件是可变对象(如 List),外部拿到引用后仍可修改内部状态,从而破坏"不可变"的预期。需要对外暴露集合时,应在访问器中返回防御性拷贝或使用 List.copyOf。
5.2 序列化与持久化的注意点
Record 可用于 JSON 序列化(Jackson 自 2.12 起支持),但在 JPA 实体里要谨慎:Record 没有无参构造器、字段不可变,直接映射会带来主键生成与字段写回的麻烦。适合用 Record 的是查询结果的只读投影(projection),而不是可写实体。关于持久层的性能取舍,可参考 MyBatis 与 JPA 性能优化:N+1、批量与缓存实战。
| 场景 | 推荐 | 原因 |
|---|---|---|
| API 入参/出参 DTO | Record | 不可变、易序列化 |
| 数据库可写实体 | 传统 class | 需无参构造与字段写回 |
| 只读查询投影 | Record | 减少样板、语义清晰 |
| 含可变状态的领域对象 | 传统 class | Record 不适合 |
六、实战:用 Record 重构 API 层 DTO
最常见的落地场景是 Web 层的数据契约。传统 VO 里大量 getter/setter 与 JSON 框架纠缠,换成 Record 后一眼就能看出"这是什么数据、能不能改"。下面是一个用户注册命令对象的前后对比:
// 重构前:十多行样板
public class SignUpCmd {
private String username;
private String email;
private String password;
// getter / setter 若干
}
// 重构后:一行声明,配合校验注解
public record SignUpCmd(
@NotBlank String username,
@Email String email,
@Size(min = 8) String password
) {}
在 Spring MVC 中,Record 可以直接作为控制器入参或出参,框架会自动完成绑定、校验(@Valid)与 Jackson 序列化;组件上的 Bean Validation 注解照常生效。对于"只读查询投影",也可以用 Record 接收 JPA 或 MyBatis 的查询结果,既减少样板又明确表达"这是不可变快照"的语义——这与我们在 MyBatis 与 JPA 性能优化 里强调的"查询与写入分离"思路是吻合的。需要提醒的是:可写实体仍建议保留传统 class,因为 Record 缺少无参构造与字段写回能力,直接映射会踩坑。
七、版本兼容与迁移建议
Record 在 JDK 16(2021)作为正式特性登场,模式 instanceof 同期可用;模式 switch 在 JDK 17 预览、JDK 21 正式定稿。生产环境建议使用 JDK 21 LTS 以获得稳定的全部能力。迁移老代码时,可优先把纯数据载体(DTO、配置项、元组式返回值)改为 Record;对大量 instanceof + 强转 的分支,用模式匹配重构能显著降低空指针与转型异常。若你在高并发服务里使用这些特性,配合 Java 21 虚拟线程 与 Spring 事务传播机制 的工程实践会更顺手;而需要编译期生成这些样板时,也可复习 Java 注解处理器实战。
八、总结
Java Record 与模式匹配不是语法糖那么简单:Record 用一行声明消灭样板并强制不可变,模式匹配把"判型—转型—分支"收敛成类型安全的一次到位。两者组合还能优雅地实现代数数据类型,让编译器替你兜底分支完整性。从今天起,遇到"只传数据的类"就先用 Record 想一想,遇到又长又臭的 if/else 类型判别就试试模式 switch。




