在Java 15之前,如果想让一个接口只能被几个指定的类实现,通常只能把接口放在特定包下,用包级私有类做间接控制,或者靠团队规范口头约束。这种做法在编译期和运行期都缺乏强制力,任何人都可以在项目其他地方写出新的实现类。Java 15正式引入的密封类(sealed class)与密封接口(sealed interface)特性,让开发者能够在语法层面精确控制哪些类可以成为子类或实现类。

什么是sealed密封类与密封接口
密封类型使用sealed修饰符声明,并通过permits子句显式列出允许扩展或实现它的类。编译器会保证只有这些被列出的类才能继承或实现该类型,其他任何类尝试这么做都会直接编译失败。这把类型的开放程度从“运行时约定”提升为“编译期强制”。
对于接口而言,使用sealed修饰后,它不再是任意类都可实现的公开契约,而是一组封闭的实现族。每个被许可的实现类必须明确声明自己是final、sealed还是non-sealed,以此决定这一支类型层次是否还能继续向下延伸。这种三态设计既保留了必要的扩展性,又杜绝了无限制的泛化。
基础语法与代码示例
下面是一个密封接口及其被许可实现类的最小示例。注意permits后面跟的是具体类名的清单,且这些类必须和接口在同一模块或同一包中(Java 17起允许跨包但需同模块)。
// 定义密封接口,只允许 Dog 和 Cat 实现
public sealed interface Animal permits Dog, Cat {
String name();
}
// final 实现类,不能再被继承
public final class Dog implements Animal {
public String name() {
return "狗";
}
}
// non-sealed 实现类,可以被其他类随意继承
public non-sealed class Cat implements Animal {
public String name() {
return "猫";
}
}
在上面的代码中,如果新建一个class Bird implements Animal,编译器会提示“Bird is not allowed to extend sealed interface Animal”。这就是密封机制最直接的约束力。同时,Cat被标记为non-sealed,意味着它自己可以被任意类继承,而Dog是final,彻底封死分支。
如果希望类型层次继续但依然可控,也可以把实现类再声明为sealed并继续用permits限制。例如把Cat改为sealed并只允许BlackCat和WhiteCat继承,就能构建一棵完全由开发者掌控的继承树。
sealed与record、enum的配合
Java的record类型默认是final的,因此它非常适合作为密封接口的实现类,用来表达固定的几种数据形态。在模式匹配(pattern matching)普及后,密封类型配合switch表达式可以实现穷尽检查,编译器能确认所有分支都已覆盖。
public sealed interface Shape permits Circle, Rectangle {}
public record Circle(double r) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
// 穷尽的模式匹配,无需 default 分支
public static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rectangle r -> r.w() * r.h();
};
}
上面代码里Shape是密封接口,只有两种实现。在switch中如果漏掉任一情况,编译器会报错,因为密封保证了不会有第三种Shape出现。相比过去写一堆if (x instanceof ...),这种方式更安全也更清晰。
枚举(enum)本身已经限定了实例数量,但如果想表达“行为随类型变化”的封闭集合,用密封接口加record往往比enum更灵活,因为每个实现可以携带不同结构的数据,而enum常量通常结构一致。
跨包与模块中的使用注意
在Java 17及以后版本,密封类型允许permits列出的类位于不同包,但必须处于同一个模块(module)内;如果项目没有使用模块系统,则必须同包。这是为了在保持封装的同时,不让类型层次泄露到不可控的依赖中。
| 声明位置 | permits类位置要求 |
|---|---|
| 无模块(classpath) | 必须与密封类型同包 |
| 有模块(module) | 可与密封类型同模块不同包 |
若你在类库中使用密封接口,建议配合module-info.java导出包含接口和实现类的包,但不要把实现类暴露为公开API,这样调用方只能依赖接口,却无法自行实现,从而彻底锁定契约。
另外要注意,反射在默认情况下也能创建子类,但如果类被标记为final或密封且未授权,某些框架可能需显式开放。使用序列化或依赖注入框架时,应确认它们支持密封类型,否则可能出现代理类生成失败的问题。
常见误区与总结
一个常见误区是认为sealed只是“更严格的abstract”,其实核心差异在于它把“谁可以继承”写进了类型系统,而不是留给人工规范。另一个误区是给所有实现类都标final,导致未来无法扩展;合理做法是核心分支用final,需要再分层的用sealed,明确开放的用non-sealed。
密封特性不是用来替代访问修饰符的,而是用来约束类型层次的边界。它和private、protected解决的问题不同,前者管“谁能看见”,后者管“谁能成为我”。
总体来看,借助Java 15的sealed特性,我们可以用很少的语法成本把接口实现牢牢限制在特定类上,既提升了代码可维护性,也让模式匹配等现代写法更加可靠。对于领域模型、协议定义、状态机等场景,密封类型应当成为首选工具。
sealed_classJava_interfacepermitted_subclasses修改时间:2026-08-07 13:27:33