密封类(sealed)
本教程共 100 篇 · 第 48 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:理解密封类如何用
sealed+permits精确限定子类范围,看清它和final、和普通继承的区别,并知道它在哪种场景下特别有用。
final 太死板,普通继承太开放
前面讲过,final 类谁都不能继承——一刀切,完全封闭。
而普通类(不加任何修饰)谁都能继承——完全开放。随便哪个包、哪个模块都能派生一个子类,父类作者根本控制不了。
现实里很多设计想要的是中间状态:我允许「指定的几个」子类继承,另外全部禁止。比如表达「图形」只有圆、矩形、三角形三种,不该出现第四种没人维护的图形类。
Java 17 正式引入的**密封类(sealed class)**就是干这个的。(它在 Java 15、16 是预览特性,到 Java 17 才转正,对应 JEP 409。)
用 sealed + permits 限定子类
写法是在 class 前加 sealed,再用 permits 列出「被允许的子类」:
public sealed class Shape permits Circle, Rectangle, Triangle {
// ...
}
意思是:只有 Circle、Rectangle、Triangle 这三个类能继承 Shape,其它任何类想 extends Shape 都会被编译器拒绝。
Note
permits后面跟的是直接子类的全称(同包下写简单名即可,跨包要写完整类名)。编译器会校验:列表里的类确实存在、且确实继承了这个密封类。
子类必须三选一
密封类的子类不能是普通类,必须在 final、sealed、non-sealed 里选一个声明:
// 1) final:到此为止,不能再有下一代
public final class Circle extends Shape { }
// 2) sealed:我继续密封,再指定自己的子类
public sealed class Rectangle extends Shape permits Square { }
// 3) non-sealed:我主动开放,允许任意类继承我
public non-sealed class Triangle extends Shape { }
这三种身份把继承树「分层管控」起来了:
final:封死末端。sealed:继续往下限量授权。non-sealed:在某个分支上重新完全开放。
Warning少写其中一种修饰符会编译报错。密封类的子类必须显式声明自己是
final/sealed/non-sealed之一,不能悄悄当普通类。
同文件可省略 permits
如果密封类和它的子类写在同一个源文件里,可以省略 permits——编译器会自己扫描这个文件,把里面的子类认作「被许可的」。
sealed class Shape {
// 没有 permits,但下面三个类在同一文件里
}
final class Circle extends Shape { }
final class Rectangle extends Shape { }
final class Triangle extends Shape { }
Tip小项目里把密封类家族放一个文件,写起来更省事。但代码多了建议还是用
permits显式列出,可读性更好,也方便子类搬到别的文件。
为什么需要密封类:给「穷举」提供依据
密封类最大的实战价值,是和 switch 模式匹配(Java 21 转正,JEP 441)配合,让编译器帮你确认「所有情况都覆盖了」。
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2.0;
// 因为 Shape 是密封的,子类只有这三种,编译器知道不用写 default
};
}
因为 Shape 的子类范围被 permits 钉死了,编译器知道不可能出现第四种,于是 switch 可以「穷举」所有分支,你漏掉任何一种它都会报错。这在普通继承下是做不到的——普通类的子类数量未知,编译器不敢假设你列全了。
Note这里用到了记录类(record)的组件访问器(
c.radius())和 switch 模式匹配。switch模式匹配的版本标注:Java 21 转正(JEP 441,预览于 17–20),本教程统一以 Java 25 LTS 语法为准。
完整可运行示例
密封类家族通常把字段设成 final(不可变),配合构造方法使用。下面用「坐标 + 形状」做一个最小可运行示例:
public sealed class Shape permits Circle, Rectangle {
public final double x;
public final double y;
public Shape(double x, double y) { this.x = x; this.y = y; }
}
final class Circle extends Shape {
public final double radius;
public Circle(double x, double y, double radius) {
super(x, y);
this.radius = radius;
}
}
final class Rectangle extends Shape {
public final double w, h;
public Rectangle(double x, double y, double w, double h) {
super(x, y);
this.w = w; this.h = h;
}
}
public class Main {
public static void main(String[] args) {
Shape s = new Circle(0, 0, 5);
System.out.println(s instanceof Circle); // true
}
}
Note这里的
x、y、radius等都声明为final,只是本例的个人选择(不可变数据更安全),并不是密封类的语法要求。密封类本身只管「谁能继承」,不管字段能不能改。
密封接口 + record:现代 Java 的黄金组合
sealed 不只能修饰类,也能修饰接口,同样用 permits 指定允许的实现类。配合 record,几行代码就能描述一个「有限类型家族」:
public sealed interface Shape permits Circle, Rectangle, Triangle { }
record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }
record Triangle(double base, double height) implements Shape { }
三行 record 就把三种形状定义完了,字段、构造方法、访问器、equals、toString 全由编译器自动生成。
Note
record天生就是final的,所以它实现密封接口时不用再写final。这是「子类必须三选一」这条规则的唯一例外,普通类仍然要老老实实选一个。
配上 switch 模式匹配,处理逻辑一眼看穿:
static String name(Shape s) {
return switch (s) {
case Circle c -> "圆,半径 " + c.radius();
case Rectangle r -> "矩形 " + r.width() + "×" + r.height();
case Triangle t -> "三角形,底 " + t.base();
};
}
违规时编译器怎么提醒你
密封类的几条硬约束,违反哪条报错都很直白。
不在 permits 名单里却想继承:
error: class Star cannot extend sealed class Shape
(as it is not listed in its permits clause)
子类忘了写 final / sealed / non-sealed:
error: sealed, non-sealed or final modifiers expected
switch 没把密封家族穷举完:
error: the switch expression does not cover all possible input values
前两条在编译期就拦住了,第三条更实用——它意味着以后往家族里加一个新子类,所有没跟着改的 switch 都会立刻报错,而不是等到运行时才发现漏了分支。
密封类与枚举:别搞混
密封类和枚举都表达「有限种类」,容易弄混,记住区别:
- 枚举的每个值是同一个类的一个固定实例(比如
Status.PENDING),重点是「值有限」; - 密封类的每个子类是不同的类(比如
Circle、Rectangle),重点是「类型有限,且各自可有不同字段和行为」。
简单说:要的是「几个固定常量」用枚举;要的是「几种不同类型、各有各的结构」用密封类。
常见疑问
Q:permits 里的子类必须和密封类放在同一个包吗?
没有模块化时必须同包,有具名模块时可以跨包但要在同一模块内。日常写代码把整个家族放进一个包最省心。
Q:non-sealed 中间的连字符能省吗?
不能。non-sealed 是 Java 里唯一带连字符的关键字,少一个横线就变成两个不认识的标识符,编译直接失败。
Q:密封类和 final 到底该怎么选?
只要不打算让任何人继承,就用 final,简单直接。只有当你确实需要「一小撮受控子类」时才上 sealed——它带来的额外收益是 switch 能穷举,代价是每加一个子类都要改 permits。
小结
sealed类用permits列出「允许的子类」,另外任何类都不能继承它(Java 17 转正,JEP 409)。- 密封类的直接子类必须声明为
final、sealed或non-sealed之一。 - 子类和密封类同文件时可省略
permits。 - 密封类在「完全封闭(final)」和「完全开放(普通类)」之间提供了可控的中间选项。
- 它和 switch 模式匹配配合,能让编译器确认分支已穷举,是表达「有限种类」的强大工具。
下一章我们讲记录类(record),它和密封类经常被一起使用,用来简洁地描述「不可变的数据」。