1. Sealed 类的语法:它是什么样子
先从 Java 面向对象的经典问题说起:开放的继承层次。在普通的 Java 中,只要你的类没有声明为 final,任何人都可以继承它。这很方便,但有时会带来意外——例如,你无法事先知道究竟哪些类会成为你的类的子类,因此也就无法保证在 switch 或 if-else 中处理了所有情况。
结果就是,当你按类型编写处理逻辑(例如在 switch 中使用模式匹配)时,不得不“以防万一”添加一个 default 分支:万一有人在某处又新建了一个子类呢?
Sealed 类解决了这个问题:它们允许显式限制子类列表。这样层次就变成封闭且可控,你的代码也会更加可预测、更安全。
基本语法
Sealed 类在 Java 17 中引入。它们通过修饰符 sealed 声明,并用关键字 permits 指定允许的子类:
public sealed class Shape permits Circle, Rectangle, Square {
// 所有图形的通用行为
}
这里我们声明了类 Shape,只有 Circle、Rectangle 和 Square 才能成为它的直接子类。其他任何人都不能扩展 Shape —— 编译器不会允许。
重要提示:在 permits 中列出的所有类必须在同一源文件中声明,或者对编译器可见(通常位于同一包)。顺便说一句,如果所有子类都与 sealed 类定义在一个文件中,那么可以省略 permits —— 编译器会自行推断。
示例:
// 全部在同一个文件中——permits 可省略
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}
对继承者的要求
每个子类都必须显式声明自己的状态:
- 声明为 final(禁止继续被继承),
- 或声明为 sealed(并继续向下限制继承),
- 或声明为 non-sealed(允许继承,取消限制)。
示例:
public sealed class Shape permits Circle, Rectangle, Square {}
public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
- Circle — 最终类,不能再被继承。
- Rectangle — 本身是 sealed,只允许两个子类。
- Square — non-sealed,任何人都可以扩展。
最小示例
public sealed class Animal permits Dog, Cat {}
public final class Dog extends Animal {}
public final class Cat extends Animal {}
试着声明一个新类 public class Wolf extends Animal {} —— 你会得到编译错误:
class Wolf is not allowed to extend sealed class Animal
2. Sealed 类的应用:何时以及为何使用
模式匹配与 switch
Sealed 类与 switch 中的模式匹配尤其契合。如果编译器知道所有可能的子类,它就能确认你是否处理了每一种情况,甚至不再要求提供 default 分支。
public sealed interface Result permits Success, Error {}
public final class Success implements Result {
public final String data;
public Success(String data) { this.data = data; }
}
public final class Error implements Result {
public final String message;
public Error(String message) { this.message = message; }
}
public class Main {
public static void main(String[] args) {
Result result = new Success("Great!");
switch (result) {
case Success s -> System.out.println("成功:" + s.data);
case Error e -> System.out.println("错误:" + e.message);
}
}
}
编译器知道 Result 不会有其他变体,因此不需要 default。如果你漏掉了某个分支,编译器会立刻提醒你。
请注意:从 Java 21 开始,如果在 switch 中没有处理所有变体,你会得到编译错误。在更早的版本(17–20)中可能仍需要 default,但 IDE 也会提示覆盖不完整。
安全与可控性
Sealed 类让开发者可以完全控制层次结构。这对领域模型尤为重要,其中变体集合需要是固定的(例如订单状态:New、Paid、Cancelled)。
简化维护与演进
当你明确知道所有子类时,添加新特性、维护代码和进行重构都会更容易。IDE 也“知道”所有的变体,能在自动补全和分析方面提供帮助。
3. Sealed 类的实践示例
示例 1:几何图形
public sealed interface Shape permits Circle, Rectangle, Square {}
public final class Circle implements Shape {
public final double radius;
public Circle(double radius) { this.radius = radius; }
}
public final class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
public final class Square implements Shape {
public final double side;
public Square(double side) { this.side = side; }
}
现在可以安全地使用带有模式匹配的 switch:
Shape shape = new Circle(5);
switch (shape) {
case Circle c -> System.out.println("圆,半径为 " + c.radius);
case Rectangle r -> System.out.println("矩形 " + r.width + "x" + r.height);
case Square s -> System.out.println("正方形,边长为 " + s.side);
}
示例 2:金融交易
public sealed interface Transaction permits Deposit, Withdraw, Transfer {}
public final class Deposit implements Transaction {
public final double amount;
public Deposit(double amount) { this.amount = amount; }
}
public final class Withdraw implements Transaction {
public final double amount;
public Withdraw(double amount) { this.amount = amount; }
}
public final class Transfer implements Transaction {
public final double amount;
public final String toAccount;
public Transfer(double amount, String toAccount) {
this.amount = amount;
this.toAccount = toAccount;
}
}
现在在处理器中可以确信没有漏掉任何交易类型:
Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
case Deposit d -> System.out.println("存款:" + d.amount);
case Withdraw w -> System.out.println("取款:" + w.amount);
case Transfer t -> System.out.println("转账:" + t.amount + " 到 " + t.toAccount);
}
示例 3:结合 non-sealed 的受限层次
public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}
public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}
// 现在任何人都可以继承 SmsNotification
public class ViberNotification extends SmsNotification {}
4. Sealed 类的特点、限制与细节
修饰符要求
- Sealed 类必须通过 permits 显式列出所有子类。
- 所有子类必须是 final、sealed 或 non-sealed。
- 子类必须在同一文件中声明,或对编译器可见。
抽象的 sealed 类
Sealed 类可以是抽象类、interface,也可以是普通类。例如:
public sealed abstract class Expr permits Const, Add, Mul {}
与其他修饰符的兼容性
- 不能把一个 sealed 类声明为 final 或 non-sealed。
- interface 也可以是 sealed(而且非常好用!)。
与 record 类的配合使用
Record 类可以作为 sealed 类的子类,只要它们是 final(默认情况下 record 总是 final):
public sealed interface Expr permits Const, Add, Mul {}
public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}
5. 有用的细节
Sealed 类与模式匹配:有什么关系
Sealed 类的关键好处是穷尽的模式匹配。听起来吓人,其实很简单。编译器知道所有可能的变体,于是你可以放心写不带 default 分支的 switch:
Expr expr = ...;
switch (expr) {
case Const c -> ...
case Add a -> ...
case Mul m -> ...
}
如果你没处理某个变体,编译器就不会让项目通过构建——既方便又安全。
在真实场景中的应用
Sealed 类在哪些地方真正有用?凡是你有固定变体集合的地方:
- 操作结果:Success、Error(如上例)。
- 订单状态:New、Paid、Cancelled。
- 解析器的抽象语法树(AST)。
- API 响应:Ok、NotFound、Error。
- 系统事件:UserLoggedIn、UserLoggedOut、UserRegistered。
6. 使用 sealed 类时的常见错误
错误 #1:忘记在 permits 中列出所有子类。
如果没有通过 permits 列举所有需要的子类,编译器会立即报错。比如你写了 permits Circle, Rectangle,却漏了 Square,而项目中确实有这个类——就会出错。
错误 #2:子类没有声明为 final、sealed 或 non-sealed。
如果子类没有使用上述任一修饰符,编译器会报错:“Class must be either final, sealed or non-sealed”。
错误 #3:子类声明在其他文件中且对编译器不可见。
列在 permits 中的所有类必须对编译器可见——要么在同一文件中,要么在同一包中。
错误 #4:在 switch 中没有处理所有变体。
如果你在带模式匹配的 switch 中使用 sealed 类却漏掉了某个分支,编译器不会让项目构建通过。这是好事——你不会漏掉任何情况。
错误 #5:尝试从未写在 permits 中的 sealed 类进行继承。
如果你试图创建一个不在 permits 列表中的子类,会报错:“is not allowed to extend sealed class”。
错误 #6:在旧版 JDK 上使用 sealed 类。
Sealed 类仅在 Java 17 及以上版本出现。如果你在更老的版本中尝试使用,会得到编译错误,甚至无法构建项目。
GO TO FULL VERSION