如何通过Sealed Classes构建领域驱动设计中的受限代数数据类型?
- 内容介绍
- 相关推荐
本文共计848个文字,预计阅读时间需要4分钟。
Java 的密封类(`sealed`)配合 `record`,为数据类型(ADT)的量身定制提供了原生支持。它不依赖于接口模拟或抽象类,也不依赖手写的子类模板,而是通过编译器强制保证:
典型场景比如支付方式、订单状态、解析结果这类“有限且封闭”的业务概念——它们天然不是开放扩展的,而是有明确、固定种类的。这时候用 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 实现放在同一个文件里,尤其是初期建模阶段。等结构稳定后再按需拆分,并同步处理模块声明。
本文共计848个文字,预计阅读时间需要4分钟。
Java 的密封类(`sealed`)配合 `record`,为数据类型(ADT)的量身定制提供了原生支持。它不依赖于接口模拟或抽象类,也不依赖手写的子类模板,而是通过编译器强制保证:
典型场景比如支付方式、订单状态、解析结果这类“有限且封闭”的业务概念——它们天然不是开放扩展的,而是有明确、固定种类的。这时候用 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 实现放在同一个文件里,尤其是初期建模阶段。等结构稳定后再按需拆分,并同步处理模块声明。

