密封类本质是声明继承契约而非限制继承,用于业务上仅有有限合法变体的场景(如订单状态),需满足permits存在性、子类修饰符明确性及包/模块一致性三条件。

密封类不是“限制继承”,而是“声明继承契约”
很多人第一反应是用 sealed 去“防别人乱写子类”,这方向就偏了。真正起作用的,是把领域模型中那些**业务上本就只有有限几种合法变体**的地方,用编译器强制对齐。比如订单状态、支付方式、消息类型——这些不是“可能有新类型”,而是“新增必须显式纳入设计”。一旦你把它当成契约来写,permits 就不再是语法负担,而是设计文档的可执行版本。
必须满足的三个硬性条件,缺一不可
Java 17 的密封类在编译期做三重校验,任意一条不满足都会直接报错,不是运行时警告:
-
permits列表里的每个类必须真实存在,且是直接继承该密封类(不能隔代) - 每个被
permits的子类,必须明确声明为final、sealed或non-sealed—— 没有默认值,漏写就编译失败 - 所有类(密封父类 + 所有
permits子类)必须在同一个包下(未模块化项目),或同一模块内且已通过module-info.java显式exports(模块化项目)
子类修饰符选哪个?取决于你是否允许“下游再扩展”
别凭感觉选 final。每种修饰符代表一种设计意图:
-
final:这个分支彻底收口,比如PendingStatus不该再有子类,业务语义已完整 -
sealed:你预期它还有细分,但必须受控,比如PaymentMethod下的CreditCard可能再分VisaCard和MasterCard,那就让CreditCard自身也sealed并permits它们的子类 -
non-sealed:极少见,仅用于框架层预留扩展点(如测试 Mock 类需继承但又不进主逻辑),用之前先问自己:这个开放是不是真的不可绕过?
和 switch 模式匹配配合时,最容易忽略的坑
密封类最大的实操价值,在于让 switch 表达式真正“穷尽”:
- 当
switch的入参是密封类类型,且所有permits子类都已作为case出现时,编译器允许你省略default分支——这不是偷懒,是类型安全的体现 - 但如果你在
permits里加了一个新子类,而某个switch没更新,编译器会立刻报错:“not exhaustive”,而不是等上线后进default导致 NPE 或逻辑跳转错误 - 注意:这个保护只对
switch(表达式或语句)生效,对传统instanceof+if/else无效——后者仍需手动维护,不推荐
真正难的不是写对语法,而是在领域建模初期就识别出哪些抽象应该密封。一旦把不该密封的类强行密封,后续演进会卡死;反过来,该密封的没封,就会在半年后发现某处 instanceof 判断突然多出一个没人认领的子类。这种边界感,只能靠对业务语义的反复确认来建立。










