很多人对 Java 的印象还停留在"啰嗦的 getter/setter 和长长的 if-instanceof"。但近几个 LTS 版本里,record、sealed 和模式匹配(pattern matching)这三件套联手,把 Java 推向了代数数据类型(ADT)和数据导向编程的范式。它们不是语法糖那么简单,背后有清晰的设计意图。这篇文章讲清三者的机制与协同。
场景:被样板代码淹没的数据类
一个不可变的数据载体,传统写法要手写构造器、所有字段的 getter、equals、hashCode、toString,动辄几十行,而且字段一改就得同步维护这一堆方法,极易出错(比如新增字段忘了加进 equals)。这类纯粹"搬运数据"的类,恰恰是 bug 和噪音的温床。
机制一:record 是不可变数据的透明载体
record 一行解决:
1 | public record Point(int x, int y) {} |
编译器自动为你生成:所有字段的 private final 声明、一个全参构造器(规范构造器 canonical constructor)、每个字段的访问器(注意是 x() 而非 getX())、基于全部字段的 equals/hashCode、以及 toString。
关键词是透明(transparent):record 的 API 完全由它的状态描述决定,状态和 API 一一对应、不可隐藏。这带来几个硬性约束:
- 字段全部
final,record 天生不可变。 - 不能继承别的类(隐式
extends Record),但可以实现接口。 - 可以加方法、静态字段、紧凑构造器做校验:
1 | public record Range(int low, int high) { |
工程上要警惕一个误区:record 的不可变是"浅不可变"。如果字段是 List 这种可变引用,外部仍能改它的内容。要真正不可变,应在紧凑构造器里做防御性拷贝(List.copyOf(...))。另外 record 的 equals 默认逐字段比较,若字段含数组要特别注意(数组的 equals 是引用比较)。
机制二:sealed 封闭继承,让类型集合"可穷举"
sealed 让你精确控制谁能继承一个类或接口:
1 | public sealed interface Shape permits Circle, Rectangle, Triangle {} |
permits 列出唯一被允许的子类型。继承者必须显式声明为 final(不可再被继承)、sealed(继续受限封闭)或 non-sealed(重新开放)三者之一。
它的意义是把"开放的继承"变成"封闭的、可穷举的类型集合"——这正是函数式语言里代数数据类型的 sum type(和类型)。编译器现在确切知道 Shape 只有三种可能。这个"穷举性知识"是模式匹配能做穷尽性检查的前提,三件套的协同点就在这里。
机制三:模式匹配,从 instanceof 到 switch
instanceof 模式匹配消除了"判断+强转"的重复:
1 | // 旧写法 |
switch 模式匹配 + record 解构才是真正的威力所在。结合 sealed,可以对类型集合做优雅且类型安全的分派:
1 | double area(Shape shape) { |
注意这里没有 default 分支也能编译通过。因为 Shape 是 sealed 的,编译器知道三种子类型已被穷尽覆盖,这就是穷尽性检查(exhaustiveness)。它的价值在演进时凸显:哪天你给 Shape 加了第四个子类型 Pentagon,所有没覆盖到它的 switch 会编译报错,强迫你处理新情况——把运行时的遗漏 bug 提前到编译期。这是 if-else 链永远做不到的。
record 解构 Circle(double r) 把模式匹配和数据结构打通:匹配类型的同时把字段拆出来绑定到变量,还能嵌套解构。再配合 when 守卫做条件细分:
1 | case Rectangle(double w, double h) when w == h -> w * w; // 正方形特判 |
工程权衡与边界
- 何时用 record,何时不用:record 适合 DTO、值对象、不可变配置、多返回值的临时聚合。但不要把 JPA 实体写成 record——实体需要无参构造器、可变状态、代理增强,与 record 的不可变和 final 天然冲突。需要可变、需要继承体系、需要封装隐藏内部状态时,仍用普通类。
- sealed 的边界:所有被 permit 的子类型必须和密封类在同一模块(或未命名模块时同一个包)内,便于编译器做穷尽性分析。这意味着 sealed 适合表达"由你掌控的、封闭的领域模型",不适合面向无限扩展的插件式 API。
- 性能与可读性:模式匹配 switch 编译后并非简单的一串 instanceof,新版会借助
invokedynamic等机制做更高效的分派,但日常无需为此操心。真正的收益是用数据导向取代繁琐的多态继承:对于"行为简单、类型固定、需要在多处做不同处理"的场景,sealed + 模式匹配比传统的访问者模式(Visitor Pattern)更直观、更少样板。 - null 处理:传统 switch 遇 null 抛 NPE,模式匹配 switch 允许显式写
case null ->分支,这是个容易忽略的改进,但也要求你主动考虑 null 情况。
小结
这三个特性不是孤立的语法糖,而是一套协同的范式升级:record 提供透明的不可变数据载体,sealed 把继承封闭成可穷举的类型集合,模式匹配则在此基础上做类型安全的解构与穷尽分派。它们合起来让 Java 能优雅地表达代数数据类型,把"数据"和"对数据的处理"清晰分离。用好它们,你写的数据建模代码会更短、更安全,而且在模型演进时由编译器替你兜底。