CodeGym /课程 /JAVA 25 SELF /Sealed 类:语法与应用

Sealed 类:语法与应用

JAVA 25 SELF
第 65 级 , 课程 0
可用

1. Sealed 类的语法:它是什么样子

先从 Java 面向对象的经典问题说起:开放的继承层次。在普通的 Java 中,只要你的类没有声明为 final,任何人都可以继承它。这很方便,但有时会带来意外——例如,你无法事先知道究竟哪些类会成为你的类的子类,因此也就无法保证在 switchif-else 中处理了所有情况。

结果就是,当你按类型编写处理逻辑(例如在 switch 中使用模式匹配)时,不得不“以防万一”添加一个 default 分支:万一有人在某处又新建了一个子类呢?

Sealed 类解决了这个问题:它们允许显式限制子类列表。这样层次就变成封闭且可控,你的代码也会更加可预测、更安全。

基本语法

Sealed 类在 Java 17 中引入。它们通过修饰符 sealed 声明,并用关键字 permits 指定允许的子类:

public sealed class Shape permits Circle, Rectangle, Square {
    // 所有图形的通用行为
}

这里我们声明了类 Shape,只有 CircleRectangleSquare 才能成为它的直接子类。其他任何人都不能扩展 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,只允许两个子类。
  • Squarenon-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 类让开发者可以完全控制层次结构。这对领域模型尤为重要,其中变体集合需要是固定的(例如订单状态:NewPaidCancelled)。

简化维护与演进

当你明确知道所有子类时,添加新特性、维护代码和进行重构都会更容易。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 显式列出所有子类。
  • 所有子类必须是 finalsealednon-sealed
  • 子类必须在同一文件中声明,或对编译器可见。

抽象的 sealed 类

Sealed 类可以是抽象类、interface,也可以是普通类。例如:

public sealed abstract class Expr permits Const, Add, Mul {}

与其他修饰符的兼容性

  • 不能把一个 sealed 类声明为 finalnon-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 类在哪些地方真正有用?凡是你有固定变体集合的地方:

  • 操作结果:SuccessError(如上例)。
  • 订单状态:NewPaidCancelled
  • 解析器的抽象语法树(AST)。
  • API 响应:OkNotFoundError
  • 系统事件:UserLoggedInUserLoggedOutUserRegistered

6. 使用 sealed 类时的常见错误

错误 #1:忘记在 permits 中列出所有子类。
如果没有通过 permits 列举所有需要的子类,编译器会立即报错。比如你写了 permits Circle, Rectangle,却漏了 Square,而项目中确实有这个类——就会出错。

错误 #2:子类没有声明为 finalsealed 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 及以上版本出现。如果你在更老的版本中尝试使用,会得到编译错误,甚至无法构建项目。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION