首页 / Java 入门教程 / 密封类(sealed)

Java 入门教程

密封类(sealed)

本教程共 100 篇 · 第 48 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程密封类sealedpermitsJava17

本节目标:理解密封类如何用 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 {
    // ...
}

意思是:只有 CircleRectangleTriangle 这三个类能继承 Shape,其它任何类想 extends Shape 都会被编译器拒绝。

Note

permits 后面跟的是直接子类的全称(同包下写简单名即可,跨包要写完整类名)。编译器会校验:列表里的类确实存在、且确实继承了这个密封类。

子类必须三选一

密封类的子类不能是普通类,必须在 finalsealednon-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

这里的 xyradius 等都声明为 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 就把三种形状定义完了,字段、构造方法、访问器、equalstoString 全由编译器自动生成。

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),重点是「值有限」;
  • 密封类的每个子类是不同的类(比如 CircleRectangle),重点是「类型有限,且各自可有不同字段和行为」。

简单说:要的是「几个固定常量」用枚举;要的是「几种不同类型、各有各的结构」用密封类。

常见疑问

Q:permits 里的子类必须和密封类放在同一个包吗?

没有模块化时必须同包,有具名模块时可以跨包但要在同一模块内。日常写代码把整个家族放进一个包最省心。

Q:non-sealed 中间的连字符能省吗?

不能。non-sealed 是 Java 里唯一带连字符的关键字,少一个横线就变成两个不认识的标识符,编译直接失败。

Q:密封类和 final 到底该怎么选?

只要不打算让任何人继承,就用 final,简单直接。只有当你确实需要「一小撮受控子类」时才上 sealed——它带来的额外收益是 switch 能穷举,代价是每加一个子类都要改 permits

小结

  • sealed 类用 permits 列出「允许的子类」,另外任何类都不能继承它(Java 17 转正,JEP 409)。
  • 密封类的直接子类必须声明为 finalsealednon-sealed 之一。
  • 子类和密封类同文件时可省略 permits
  • 密封类在「完全封闭(final)」和「完全开放(普通类)」之间提供了可控的中间选项。
  • 它和 switch 模式匹配配合,能让编译器确认分支已穷举,是表达「有限种类」的强大工具。

下一章我们讲记录类(record),它和密封类经常被一起使用,用来简洁地描述「不可变的数据」。