Java switch 已覆盖 sealed 类的所有叶子子类,为何仍不满足穷尽性?
结论:Shape 和 Quadrangle 都需要考虑自身实例
按照给出的类声明,这个编译错误符合 Java 的规则。Shape 和 Quadrangle 都没有声明为 abstract,因此都可以直接实例化。现有的三个分支只处理了叶子子类,遗漏了这两类对象。
如果设计上只允许创建 Circle、Rectangle 和 Diamond 的实例,可以将 Shape 和 Quadrangle 都声明为 abstract sealed class。这样,原来的三个类型分支就能满足穷尽性检查,无需添加 default。
为什么 sealed 没有让这些分支穷尽?
sealed 限制哪些类型可以直接继承当前类型,但不禁止创建当前类的对象。按照原始声明,在能够访问构造器的代码中,以下写法都合法:
Shape shape = new Shape();
Shape quadrangle = new Quadrangle();
这两个对象都无法匹配 Rectangle、Diamond 或 Circle 分支。
Java 的穷尽性规则允许根据 abstract sealed 类的获准子类型递归判断覆盖情况。这里的 abstract 排除了父类自身的直接实例。规则见 JLS §14.11.1.1。
方案一:将两个父类都声明为 abstract
如果 Shape 和 Quadrangle 只用于表达类型层次,可以修改类声明:
abstract sealed class Shape permits Circle, Quadrangle {}
abstract sealed class Quadrangle extends Shape
permits Diamond, Rectangle {}
final class Rectangle extends Quadrangle {}
final class Diamond extends Quadrangle {}
final class Circle extends Shape {}
然后在方法中使用以下 switch:
switch (shape) {
case Rectangle r -> System.out.println("");
case Diamond d -> System.out.println("");
case Circle c -> System.out.println("");
}
这里假设 shape 的声明类型为 Shape。Rectangle 和 Diamond 覆盖了抽象的 Quadrangle 分支,再加上 Circle,就覆盖了抽象的 Shape 层次。
两个类都需要修改。只将 Shape 声明为 abstract,仍会遗漏直接创建的 Quadrangle 对象;只修改 Quadrangle,则仍会遗漏直接创建的 Shape 对象。
方案二:保留具体父类,增加父类型分支
如果业务需要直接创建 Shape 或 Quadrangle,应为这些对象提供处理分支。例如,保留原始类声明,并使用:
switch (shape) {
case Rectangle r -> System.out.println("Rectangle");
case Diamond d -> System.out.println("Diamond");
case Quadrangle q -> System.out.println("Quadrangle");
case Circle c -> System.out.println("Circle");
case Shape s -> System.out.println("Shape");
}
case Shape s 覆盖剩余的非 null Shape 对象,因此不需要 default。更具体的类型分支必须放在对应的父类型分支前面,否则会被父类型分支支配,导致编译错误。相关规则见 JLS §14.11.1。
这种写法也会接住以后新增的子类型。如果希望新增子类型后由编译器提醒补充分支,采用抽象父类并逐一列出叶子类型更合适。
语法和 null 的注意事项
原示例是 switch 语句。由于使用了类型模式,它也必须满足穷尽性要求。
示例答案使用了具名模式变量,以避免依赖 _ 语法。原代码中的 case Rectangle _ 使用未命名模式变量,该功能在 Java 22 正式提供,在 Java 21 中属于预览功能。参见 Oracle Java 语言变更说明。
通过穷尽性检查也不表示已经处理了 null。如果 shape 可能为 null,可以按业务需要显式添加 case null。没有该分支时,上述 switch 遇到 null 会抛出 NullPointerException,单独添加 default 也不会接住它。参见 JLS §14.11.1.2。
备注:内容仅供参考。
