Java 17 正式引入密封类(Sealed Class) 作为标准特性(JEP 409),其核心目标是解决传统类继承的 “开放性失控” 问题 —— 通过显式限制类的继承关系,让类的层次结构更可控、更安全,同时提升代码的可读性和可维护性。本文将从设计原理、使用规则、限制场景到非密封类的选型,全面拆解密封类的技术细节。
一、设计原理:为什么需要密封类?
在密封类出现前,Java 类的继承存在两种极端:
- 默认的 “完全开放”:普通类可被任意包中的类继承(除非用
final修饰),导致类的行为可能被意外修改,破坏设计初衷(如ArrayList若被不当继承,可能违背其 “动态数组” 的核心逻辑); - 完全封闭(
final):类无法被任何类继承,灵活性不足(如想让Shape类仅允许Circle、Rectangle继承,final会直接禁止所有继承)。
密封类的设计初衷,就是提供一种 **“中间态的可控继承”—— 允许类被继承,但仅允许显式指定的子类 ** 继承,从而实现:
- 确定性:类的子类范围在编译期就明确,开发者无需猜测 “还有哪些未知子类”;
- 安全性:避免无关类随意继承并修改核心逻辑,减少潜在 Bug;
- 可维护性:类层次结构固定,后续迭代时无需担心 “意外扩展” 带来的连锁影响。
核心设计逻辑:“白名单” 式继承
密封类通过 sealed 关键字声明,并通过 permits 子句显式列出 “允许继承的子类”,形成一张 “继承白名单”。编译器会强制校验:
- 所有
permits列出的子类必须存在(编译期可见); - 除
permits列出的子类外,其他类无法继承该密封类; - 子类必须明确声明自身的 “继承权限”(如
final、sealed、non-sealed),避免权限扩散。
二、基础使用:密封类与子类的定义规则
密封类的使用需遵循严格的语法和权限规则,核心是 “密封类声明” 和 “子类权限约束” 两部分。
1. 密封类的声明语法
密封类需用 sealed 关键字修饰,并通过 permits 子句指定允许的子类。语法格式如下:
// 密封类声明:sealed + permits 子类列表
public abstract sealed class Shape
permits Circle, Rectangle, Triangle { // 仅允许这3个子类继承
// 抽象方法(密封类常作为“抽象基类”,也可是非抽象类)
public abstract double getArea();
}
关键说明:
permits子句必须紧跟类名,子类列表用逗号分隔;- 若密封类与子类在同一源文件中,
permits子句可省略(编译器会自动扫描同一文件中的子类); - 密封类可以是抽象类(最常见,用于定义统一接口)或非抽象类(需确保子类不破坏其逻辑)。
2. 密封类子类的权限约束
密封类的子类必须显式声明 “继承权限”,且仅允许以下三种修饰符(编译器强制校验):
| 修饰符 | 含义 | 适用场景 |
|---|---|---|
final | 子类不可再继承(“终止继承链”) | 子类逻辑稳定,无需扩展(如 Circle) |
sealed | 子类可继承,但仅允许其 permits 列出的子类(“延续密封链”) | 子类需进一步限制继承(如 Polygon) |
non-sealed | 子类可被任意类继承(“打破密封链”,回归普通类的开放性) | 子类需要灵活扩展(如 CustomShape) |
示例:三种权限的子类实现
// 1. final 子类:不可再继承
public final class Circle extends Shape {
private double radius;
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
// 2. sealed 子类:延续密封链,仅允许 Square、Rectangle 继承
public sealed class Polygon extends Shape
permits Square, Rectangle {
protected int sides; // 边数
}
// 3. non-sealed 子类:可被任意类继承
public non-sealed class CustomShape extends Shape {
private String type;
@Override
public double getArea() {
// 自定义计算逻辑
return 0.0;
}
}
// Polygon 的子类(需符合 sealed 约束)
public final class Square extends Polygon {
private double side;
@Override
public double getArea() {
return side * side;
}
}
3. 密封接口(Sealed Interface)
密封类的逻辑同样适用于接口 ——sealed 接口可限制其实现类,语法与密封类一致:
// 密封接口:仅允许 FileLogger、ConsoleLogger 实现
public sealed interface Logger
permits FileLogger, ConsoleLogger {
void log(String message);
}
// 实现类需声明权限(如 final)
public final class FileLogger implements Logger {
@Override
public void log(String message) {
// 写入文件逻辑
}
}
注意:
- 密封接口的实现类需满足与密封类子类相同的权限约束(
final/sealed/non-sealed); - 若密封接口的实现类是枚举(Enum),可省略权限修饰符(枚举默认不可继承,等价于
final)。
三、使用限制:这些场景不能用密封类
密封类的 “可控性” 依赖严格的编译期校验,因此存在以下使用限制,违反会直接编译报错:
1. 子类的可见性限制
密封类与其 permits 列出的子类,必须满足可见性兼容:
- 若密封类是
public,则所有子类必须是public(或同一模块内的protected/private,但跨模块不可见); - 若密封类是包访问权限(无修饰符),则所有子类必须在同一包中;
- 子类不能是 “匿名内部类” 或 “局部类”(无法被
permits显式引用)。
错误示例:
// 密封类为 public
public sealed class Shape permits Circle { }
// 子类为包访问权限(编译报错:public 密封类的子类必须是 public)
class Circle extends Shape { }
2. 继承与实现的限制
- 密封类不能被
enum继承(枚举默认继承Enum,Java 不支持多继承); - 密封接口的实现类若为
enum,需确保枚举不被扩展(枚举本身不可继承,符合final语义); - 密封类不能是
record(Java 16+ 的记录类),但record可作为密封类的子类(需声明final/sealed/non-sealed,record默认是final,因此可直接作为密封类子类)。
正确示例(record 作为子类):
public sealed class Shape permits Point { }
// record 默认是 final,可直接作为密封类子类
public record Point(double x, double y) extends Shape {
@Override
public double getArea() {
return 0.0; // 点无面积
}
}
3. 泛型与密封类的限制
密封类支持泛型,但需注意:permits 子句中不能使用 “泛型通配符” 或 “未指定的泛型参数”,必须明确具体类型。
错误示例:
// 错误:permits 子句中不能用通配符 <?>
public sealed class Container<T> permits List<?> { }
正确示例:
public sealed class Container<T> permits StringContainer, IntegerContainer { }
public final class StringContainer extends Container<String> { }
public final class IntegerContainer extends Container<Integer> { }
4. 模块系统下的限制
若使用 Java 模块系统(module-info.java),密封类的子类必须在同一模块内,或通过 exports/opens 显式暴露(否则模块外无法访问子类,导致 permits 子句无效)。
模块声明示例:
// module-info.java
module com.example.shapes {
// 暴露密封类及其子类所在的包
exports com.example.shapes;
}
四、非密封类(Non-Sealed):何时打破密封链?
non-sealed 是密封类子类的三种权限之一,其核心作用是 “打破密封链”—— 让子类回归普通类的 “完全开放性”,允许任意类继承。但需谨慎使用,否则会失去密封类的 “可控性” 优势。
1. 非密封类的核心特性
- 开放性:
non-sealed子类可被任意类继承(包括不同包、不同模块的类); - 兼容性:
non-sealed子类的继承规则与普通类完全一致(无需permits子句); - 过渡性:用于 “部分开放” 场景 —— 密封类控制核心子类,非密封类允许灵活扩展边缘场景。
2. 非密封类的选型场景
适合使用非密封类的场景:
- 边缘功能扩展:核心逻辑用密封类控制(如
Circle、Rectangle),边缘功能允许用户自定义(如CustomShape); - 兼容旧代码:当密封类需要与旧的 “开放继承” 代码兼容时,用
non-sealed子类作为过渡; - 框架扩展点:框架提供密封类定义核心能力,用
non-sealed子类作为 “扩展点”,允许用户基于扩展点自定义实现。
示例:框架扩展场景
// 框架核心密封类:控制核心日志实现
public abstract sealed class FrameworkLogger permits ConsoleLogger, FileLogger, CustomLoggerExtension {
public abstract void log(String message);
}
// 框架提供的非密封扩展点:允许用户自定义日志
public non-sealed class CustomLoggerExtension extends FrameworkLogger {
// 提供基础能力(如日志格式化),允许用户扩展
protected String formatMessage(String message) {
return "[" + LocalDateTime.now() + "] " + message;
}
}
// 用户自定义日志实现(基于非密封类扩展)
public class DatabaseLogger extends CustomLoggerExtension {
@Override
public void log(String message) {
String formatted = formatMessage(message);
// 写入数据库逻辑(用户自定义)
}
}
不适合使用非密封类的场景:
- 核心业务逻辑:若子类涉及核心业务规则(如
OrderStatus、PaymentMethod),应使用final或sealed,避免被意外修改; - 安全性敏感场景:若类涉及权限、加密等敏感逻辑,
non-sealed可能导致逻辑被篡改,需禁止使用; - 性能敏感场景:
non-sealed子类的继承关系不固定,可能影响 JVM 的 “类层次优化”(如final类可被 JVM 内联优化)。
3. 非密封类的风险与规避
- 风险 1:继承失控:
non-sealed子类可能被大量无关类继承,导致类层次混乱;- 规避:在非密封类中定义清晰的 “扩展契约”(如抽象方法、文档注释),限制子类的修改范围;
- 风险 2:破坏封装:子类可能直接访问非密封类的
protected成员,导致封装性下降;- 规避:尽量使用
private成员 +public方法,减少protected成员的暴露;
- 规避:尽量使用
- 风险 3:兼容性问题:若后续修改非密封类的逻辑,可能影响所有子类;
- 规避:非密封类的核心逻辑应保持稳定,扩展逻辑通过 “组合” 而非 “继承” 实现(如使用策略模式)。
五、密封类的典型应用场景
密封类并非 “银弹”,需结合具体场景使用。以下是其最适合的场景:
1. 定义有限的类型集合(代数数据类型)
密封类最经典的场景是 “代数数据类型”—— 即类的子类是有限且确定的,如:
- 订单状态(
Pending、Paid、Shipped、Cancelled); - 表达式类型(
Add、Subtract、Multiply); - 支付方式(
WeChatPay、Alipay、CreditCard)。
示例:订单状态管理
// 密封类:订单状态仅允许这4种
public abstract sealed class OrderStatus
permits Pending, Paid, Shipped, Cancelled {
public abstract String getStatusDesc();
}
public final class Pending extends OrderStatus {
@Override
public String getStatusDesc() {
return "订单待支付";
}
}
public final class Paid extends OrderStatus {
private LocalDateTime paidTime;
@Override
public String getStatusDesc() {
return "订单已支付(支付时间:" + paidTime + ")";
}
}
// 其他状态省略...
这种场景下,使用密封类可确保:
- 不会出现 “未知的订单状态”(编译期校验);
switch语句中可使用 “模式匹配”(Java 17+),确保覆盖所有状态(避免default分支)。
2. 增强 switch 模式匹配的安全性
Java 17 引入的 “模式匹配 switch”(JEP 406)与密封类高度契合 —— 若 switch 的表达式是密封类,编译器会强制校验 “是否覆盖所有子类”,避免遗漏。
示例:模式匹配 switch + 密封类
public double calculateArea(Shape shape) {
// 编译器会校验:是否覆盖了 Shape 的所有子类(Circle、Polygon、CustomShape)
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius(); // 解构 Circle
case Square s -> s.side() * s.side(); // 解构 Square(Polygon 的子类)
case Rectangle r -> r.length() * r.width(); // 解构 Rectangle
case CustomShape cs -> cs.calculateCustomArea(); // 自定义逻辑
// 无需 default:编译器确认已覆盖所有子类
};
}
若后续给 Shape 添加新子类(如 Ellipse),编译器会直接报错,提示 “switch 未覆盖所有情况”,避免 runtime 异常。
3. 框架 / 库的 API 设计
框架或库的 API 设计中,密封类可用于:
- 定义 “不可扩展的核心接口”(如
Spring的BeanDefinition若用密封类,可避免用户随意修改 Bean 定义逻辑); - 限制 “合法的实现类”(如
Jackson的JsonNode可设计为密封类,仅允许ObjectNode、ArrayNode等核心实现)。
六、密封类 vs 其他控制继承的方式
Java 中曾通过 final、private 构造器等方式控制继承,密封类与这些方式的对比如下:
| 控制方式 | 核心逻辑 | 灵活性 | 可控范围 | 适用场景 |
|---|---|---|---|---|
密封类(sealed) | 显式指定允许的子类(白名单) | 中 | 部分开放(指定子类) | 需限制继承范围,允许部分扩展 |
final 类 | 禁止所有继承 | 低 | 完全封闭 | 类逻辑稳定,无需任何扩展 |
| 私有构造器 | 子类无法调用构造器(间接禁止继承) | 低 | 完全封闭(仅内部类可继承) | 工具类(如 Math),仅允许内部扩展 |
非密封类(non-sealed) | 允许所有继承 | 高 | 完全开放 | 需灵活扩展的边缘场景 |
七、总结:密封类的最佳实践
- 优先用于 “有限类型集合”:如状态、枚举、表达式等,确保类层次结构的确定性;
- 谨慎使用
non-sealed:仅在 “边缘场景需要扩展” 时使用,核心逻辑尽量用final或sealed; - 结合模式匹配
switch:利用编译器的 “全覆盖校验”,避免遗漏子类处理; - 模块系统下注意暴露:确保密封类及其子类在模块内可见,或通过
exports显式暴露; - 避免过度设计:若类的子类数量不确定(如
List的实现类可能不断增加),不适合用密封类。
密封类并非替代 final 或普通类,而是提供了一种 “中间态的可控继承” 方案 —— 在 “完全封闭” 和 “完全开放” 之间找到平衡,让 Java 的类设计更灵活、更安全。
2万+

被折叠的 条评论
为什么被折叠?



