record 和 sealed interface 是状态机建模的最小可靠单元,因其强制穷尽子类型、确保数据不可变、支持编译期分支覆盖检查。

Record 和 sealed interface 的组合不是“更好写法”的选择,而是类型安全边界的硬性收口——只要编译通过,你就已经排除了绝大多数状态遗漏、非法分支、字段可变导致的并发问题。
为什么 sealed interface + record 是状态机建模的最小可靠单元
传统状态机常靠字符串或枚举驱动,但 String 类型无法约束数据结构,enum 又无法携带差异化字段。而 sealed interface 强制穷尽所有子类型,record 确保每个子类型的数据不可变且结构自文档化。
- 编译器能静态验证:所有
switch分支是否覆盖全部permits子类,漏写直接报错 -
record字段天然final,避免状态中途被篡改(比如PaymentSuccess被意外修改timestamp) - IDE 能基于密封性自动补全
switch分支,不依赖人工记忆或文档对齐 - 序列化/反序列化时,Jackson 或 Gson 可通过子类型名精准路由,无需额外
@JsonSubTypes配置
定义状态接口与 record 实现的三要素
必须同时满足以下三点,否则编译失败或失去穷尽性保障:
-
sealed interface必须显式声明permits,列出所有实现类(如permits OrderPlaced, PaymentFailed, ShipmentDispatched) - 每个实现类必须是
public且直接implements该接口;若用record,则字段列表需完整表达该状态所需数据(例如PaymentFailed(String reason, int code)) - 所有实现类必须与
sealed interface在同一模块中;跨模块扩展需显式声明non-sealed,但这就破坏了状态机封闭性,应避免
错误示例:public sealed interface OrderEvent permits OrderPlaced {} 编译失败——OrderPlaced 未定义或不在同一编译单元;正确写法必须紧随其后定义 public record OrderPlaced(String orderId, Instant at) implements OrderEvent。
在 switch 中做穷尽匹配时的关键细节
Java 21+ 的 switch 表达式支持模式匹配,但只有密封类型才能触发编译期穷尽检查。关键点在于:
- 必须用
switch (event) -> { ... }表达式语法,而非语句式switch (event) { case ... } - 每个
case必须是具体 record 类型,且带模式变量(如case OrderPlaced(var orderId, var at) -> {...}),不能只写case OrderPlaced: - 不允许写
default分支;一旦漏掉某个permits类型,编译直接失败,而不是运行时报MatchException - 若某 record 字段为泛型(如
OrderPlaced<T>),模式匹配仍有效,但类型擦除后无法在运行时校验,需靠编译期约束兜底
容易被忽略的边界情况:null 值与构造约束
record 不会自动拒绝 null 字段,这在状态机中极易引发空指针——比如 PaymentFailed(null, 500) 合法但语义错误。
- 必须手动在 record 中添加构造逻辑(Java 14+ 支持私有构造器 + 公共静态工厂方法):
public record PaymentFailed(String reason, int code) implements PaymentResult { public PaymentFailed { if (reason == null || reason.isBlank()) { throw new IllegalArgumentException("reason cannot be null or blank"); } if (code < 100 || code >= 600) { throw new IllegalArgumentException("invalid HTTP status code"); } } } - 不要依赖 Lombok 或 Jackson 的注解来拦截 null;它们作用于运行时,而密封性检查发生在编译期,二者脱节
- 如果状态需要复杂初始化(如解析嵌套 JSON),record 不适合直接承载,应改用
final class实现 sealed interface,并把 record 作为其内部数据载体
真正健壮的状态机,不靠测试覆盖率兜底,而靠编译器强制你把所有可能、所有约束、所有非法输入都写进类型定义里。record 和 sealed interface 就是那道编译期的铁闸。一旦放行,后续逻辑才值得信任。

















