密封类+Records是Java构建代数数据类型的自然组合,通过permits限定变体、record保证不可变、编译器强制穷尽检查,需满足三硬约束且须同模块/同文件。

密封类 + Records 是构建代数数据类型的自然组合
Java 的密封类(sealed)配合 record,就是为代数数据类型(ADT)量身定制的原生支持。它不靠枚举模拟、不靠抽象类+一堆子类手写样板,而是用编译器强制保证:所有可能的变体就只有你列在 permits 里的那几个,不多不少。
典型场景比如支付方式、订单状态、解析结果这类“有限且封闭”的业务概念——它们天然不是开放扩展的,而是有明确、固定种类的。这时候用 sealed interface 定义契约,用 record 实现具体变体,语义清晰、不可变、无冗余代码。
必须满足的三个语法硬约束
漏掉任意一条,编译直接报错,不是运行时问题:
-
sealed接口或类必须显式带permits子句(除非所有子类与它在同一源文件中,此时可省略) - 每个被
permits列出的实现类/子类,必须声明为final、sealed或non-sealed -
record实现密封接口时,必须加final(哪怕不写,record 默认就是 final,但显式写上更安全、可读性更强)
switch 表达式能做穷尽检查的关键条件
只有当所有分支都覆盖了 permits 列出的类型,且这些类型本身是 final(或至少没有额外子类),编译器才允许你省略 default 分支。否则会报错:the switch expression does not cover all possible values。
示例:
String process(PaymentMethod method) {
return switch (method) {
case CreditCard c -> "Card: " + c.number().substring(12);
case BankTransfer b -> "Bank: " + b.accountNumber();
case DigitalWallet w -> "Wallet: " + w.provider();
// 没有 default —— 因为 PaymentMethod 是 sealed interface,
// 且 CreditCard / BankTransfer / DigitalWallet 全是 final record
};
}
如果其中某个实现类是 non-sealed class,或者漏写了 case,编译就会失败。
模块和包位置容易被忽略的限制
密封类/接口与其 permits 列出的子类/实现类,**必须在同一个模块中**;如果用的是无模块项目(即默认模块),则它们必须在**同一编译单元内可见**——通常意味着:要么同在一个 .java 文件,要么在同一个模块路径下且相互可访问。
常见踩坑点:
- 把
sealed interface PaymentMethod放在domain包,却把CreditCard record放在infra包 → 编译失败 - 用了多模块 Maven 项目,但没在
module-info.java中导出密封接口所在的包,或没对实现模块添加requires→ 子类无法被识别为合法许可类型 - 子类用了
protected构造器或包私有访问控制,导致密封类无法“看到”其可实例化性 → 编译报class is not accessible
最稳妥的做法:把密封接口和它的所有 record 实现放在同一个文件里,尤其是初期建模阶段。等结构稳定后再按需拆分,并同步处理模块声明。


















