Java Record 与模式匹配实战:写出更简洁的类型安全代码

还在为每个 DTO、VO 写一堆 private final 字段、getter、equalshashCodetoString 吗?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) {} 中的 xy组件(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。下面这张表列出了关键差异。

维度传统 classRecord
样板代码需手写 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 入参/出参 DTORecord不可变、易序列化
数据库可写实体传统 class需无参构造与字段写回
只读查询投影Record减少样板、语义清晰
含可变状态的领域对象传统 classRecord 不适合

六、实战:用 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

上一篇 Java 定时任务调度:@Scheduled 与 Quartz
下一篇 PostgreSQL 物化视图实战:预计算加速慢报表查询