Java 15中声明sealed接口需用public sealed interface X permits A, B {},且每个实现类必须显式声明为final、sealed或non-sealed;跨模块时须正确配置exports和requires。

Java 15 sealed类怎么声明允许实现的子类
Java 15 引入 sealed 关键字,能明确限定哪些类可以继承或实现某个类/接口。想让接口只被特定类实现,必须用 sealed 声明接口,并用 permits 列出允许实现它的具体类名。
常见错误是只加 sealed 却漏写 permits,编译直接报错:sealed type must have a permits clause;或者把类名写错(大小写、包名缺失),导致编译器找不到对应类型。
-
permits后面只能跟**已存在且可访问的类名**,不能是通配符或表达式 - 被允许的实现类必须用
final、sealed或non-sealed显式声明自身继承策略 - 如果实现类在另一个模块,需确保模块声明中导出了该类所在包
public sealed interface PaymentStrategy
permits CreditCardPayment, AlipayPayment {}
实现类必须显式声明自己的密封状态
哪怕你只想让一个类实现接口,这个类也不能“默认自由”——它必须用 final、sealed 或 non-sealed 表明立场。否则编译失败:class X is not allowed to extend sealed interface Y。
这是最容易忽略的一环:开发者以为只要在接口上写了 permits 就够了,结果卡在实现类编译不过。
立即学习“Java免费学习笔记(深入)”;
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
-
final:最常用,表示该实现类不能再被继承(适合简单策略类) -
sealed:该实现类自己也限制子类,需配合自己的permits -
non-sealed:开放继承,但仅限当前模块内(跨模块仍受模块系统约束)
public final class CreditCardPayment implements PaymentStrategy { ... }
sealed接口在模块系统中的可见性陷阱
如果 PaymentStrategy 接口和它的 permits 类不在同一个模块,默认无法访问——哪怕它们在同一个包里。JVM 会报 class X is not in the permits list of sealed interface Y,表面看是列表问题,实则是模块未导出或未读取。
典型场景:你在 payment.api 模块定义接口,在 payment.impl 模块写实现类,但 payment.api 的 module-info.java 没导出接口所在包,或 payment.impl 没声明 requires payment.api。
- 接口模块必须用
exports导出接口所在包 - 实现类模块必须用
requires依赖接口模块 - 若实现类是
non-sealed,且希望其他模块继承它,还需额外exports它的包
sealed不是运行时限制,而是编译期契约
sealed 的所有检查都发生在编译阶段。JVM 运行时不会验证是否真有且仅有 permits 中的类实现了该接口——也就是说,通过反射、动态代理、字节码增强等手段,依然可能绕过限制。
这意味着它本质是 API 设计者与调用者之间的“约定”,不是安全围栏。如果你需要强隔离(比如插件系统防滥用),得配合模块系统、安全管理器或自定义类加载器。
- IDE 和 javac 能提前发现非法继承,提升协作清晰度
- 不适用于需要运行时动态扩展的场景(如 Spring 的 @Component 扫描)
- Java 17+ 对 sealed 支持更稳,Java 15 是预览特性,需加
--enable-preview
sealed 的真正价值不在“拦住谁”,而在让继承关系变成可读、可查、可维护的显式契约——这点常被当成语法糖,其实它改的是设计沟通成本。

















