Java Sealed Class:编译器如何实现对子类集合的静态检查与验证
大家好,今天我们来深入探讨Java Sealed Class,特别是编译器如何实现对子类集合的静态检查与验证。Sealed Class是Java 17引入的一个重要特性,它允许我们限制一个类的子类集合,从而在编译时提供更强的类型安全性和模式匹配能力。理解编译器如何处理Sealed Class对于我们更好地利用这个特性至关重要。
什么是Sealed Class?
在传统的面向对象编程中,一个类可以被任意数量的其他类继承。这在某些情况下是很有用的,但也可能导致代码的不可预测性和难以维护性。Sealed Class通过显式声明允许继承的子类来解决这个问题。
一个Sealed Class必须使用 sealed 关键字声明,并且必须使用 permits 子句明确列出允许继承的子类。这些子类必须与Sealed Class在同一个模块或同一个包中(Java 17及之前的版本),Java 19及以后版本允许在任何模块或包中。
示例:
sealed class Shape permits Circle, Rectangle, Square {
    // Shape类的通用属性和方法
}
final class Circle extends Shape {
    double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
}
final class Rectangle extends Shape {
    double width;
    double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}
final class Square extends Shape {
    double side;
    public Square(double side) {
        this.side = side;
    }
}在这个例子中,Shape 是一个Sealed Class,它只允许 Circle、Rectangle 和 Square 这三个类继承它。任何其他类尝试继承 Shape 都会导致编译错误。  注意,Circle, Rectangle, Square 必须是 final, sealed, 或者 non-sealed,否则会导致编译错误。
编译器如何处理Sealed Class?
编译器在处理Sealed Class时,主要进行以下几个方面的静态检查和验证:
- 
子类集合的完整性检查: 编译器会验证 permits子句中列出的所有类是否确实是Sealed Class的直接子类。如果permits子句中包含的类不是Sealed Class的直接子类,编译器会报错。
- 
子类类型的限制: 编译器强制要求Sealed Class的所有直接子类必须是 final、sealed或non-sealed的。- final子类不能被继承。
- sealed子类必须声明自己的- permits子句,进一步限制其子类。
- non-sealed子类可以被任意类继承,打破了Sealed Class的限制。
 
- 
同一模块或包的限制(Java 17及之前): 编译器会检查Sealed Class及其所有直接子类是否位于同一个模块或同一个包中。如果不在同一个模块或同一个包中,编译器会报错。 Java 19之后,这个限制放宽了。 
- 
模式匹配的完备性检查: 编译器在进行模式匹配时,会利用Sealed Class的信息来检查模式匹配是否完备。如果模式匹配没有覆盖Sealed Class的所有子类,编译器会发出警告或错误。 
让我们更深入地了解这些检查是如何进行的。
1. 子类集合的完整性检查
编译器在编译Sealed Class时,首先会解析 permits 子句,并收集其中列出的所有类名。然后,编译器会检查这些类是否确实是Sealed Class的直接子类。
示例:
sealed class Animal permits Dog, Cat { }
final class Dog extends Animal {}
final class Cat extends Animal {}
//final class Fish {} // This class is not a permitted subclass在这个例子中,编译器会检查 Dog 和 Cat 是否是 Animal 的直接子类。如果存在一个类(例如 Fish)不是 Animal 的子类,但被错误地包含在 permits 子句中,编译器会报错。
编译器的具体实现步骤:
- 解析 permits子句: 编译器读取sealed class Animal permits Dog, Cat声明,提取Dog和Cat作为潜在的子类。
- 类型检查: 编译器检查 Dog和Cat的类声明,确认它们是否直接继承自Animal。 这涉及到符号解析和类型关系分析。 编译器会构建一个类层次结构图,并验证Dog和Cat在这个图中的位置。
- 错误报告: 如果 permits子句中包含的类不是Animal的直接子类,编译器会生成一个错误信息,例如:"Class ‘Fish’ is not a permitted subclass of ‘Animal’"。
2. 子类类型的限制
编译器强制要求Sealed Class的所有直接子类必须是 final、sealed 或 non-sealed 的。这是为了确保Sealed Class的子类集合是可预测和可控的。
- 
final子类:final子类不能被继承,因此它们是Sealed Class子类集合的叶子节点。
- 
sealed子类:sealed子类可以进一步限制它们的子类,从而形成更复杂的类型层次结构。
- 
non-sealed子类:non-sealed子类可以被任意类继承,打破了Sealed Class的限制。使用non-sealed子类意味着你放弃了对该分支的封闭性保证。
示例:
sealed class Shape permits Circle, Rectangle, OpenShape {}
final class Circle extends Shape {
    double radius;
}
sealed class Rectangle extends Shape permits Square {
    double width;
    double height;
}
final class Square extends Rectangle {
    double side;
}
non-sealed class OpenShape extends Shape {
    // can be extended by any class
}在这个例子中,Circle 是 final 的,Rectangle 是 sealed 的,而 OpenShape 是 non-sealed 的。编译器会检查这些类的声明,确保它们满足Sealed Class的限制。
编译器的具体实现步骤:
- 子类类型检查: 对于 Shape的每一个permits子句中的类,编译器会检查其声明中是否包含final、sealed或non-sealed关键字。
- 错误报告: 如果子类没有声明 final、sealed或non-sealed关键字,编译器会报错。例如,如果Rectangle没有sealed关键字,编译器会生成一个错误信息:"Permitted subclass ‘Rectangle’ of sealed class ‘Shape’ must be declared ‘final’, ‘sealed’ or ‘non-sealed’"。
- 递归检查: 如果子类是 sealed的,编译器会递归地检查其permits子句中列出的所有类,确保它们也满足Sealed Class的限制。
3. 同一模块或包的限制(Java 17及之前)
在Java 17及之前的版本中,Sealed Class及其所有直接子类必须位于同一个模块或同一个包中。这是为了简化编译器的实现,并确保Sealed Class的子类集合是可预测的。
示例:
// Package: com.example
sealed class Animal permits Dog, Cat {}
final class Dog extends Animal {}
final class Cat extends Animal {}
// Package: com.example.another
// final class Fish extends Animal {} // Compilation error in Java 17 and earlier在这个例子中,Animal、Dog 和 Cat 位于同一个包 com.example 中。如果 Fish 位于不同的包 com.example.another 中,编译器会报错。
编译器的具体实现步骤:
- 包或模块检查: 编译器会记录Sealed Class的包或模块信息。然后,对于 permits子句中的每一个类,编译器会检查它们的包或模块信息是否与Sealed Class相同。
- 错误报告: 如果子类的包或模块信息与Sealed Class不同,编译器会报错。例如,如果 Fish位于不同的包中,编译器会生成一个错误信息:"Permitted subclass ‘Fish’ of sealed class ‘Animal’ must be in the same package or module as ‘Animal’"。
Java 19的改进:
Java 19放宽了这一限制,允许Sealed Class的子类位于不同的模块或包中。这使得Sealed Class更加灵活,可以用于更广泛的场景。
4. 模式匹配的完备性检查
Sealed Class的一个主要优点是它可以用于模式匹配,并且编译器可以检查模式匹配是否完备。这意味着编译器可以确保你的模式匹配代码覆盖了Sealed Class的所有子类。
示例:
sealed class Result<T> permits Success, Failure {}
final class Success<T> extends Result<T> {
    T value;
    public Success(T value) {
        this.value = value;
    }
}
final class Failure<T> extends Result<T> {
    String error;
    public Failure(String error) {
        this.error = error;
    }
}
public class Main {
    public static void main(String[] args) {
        Result<Integer> result = new Success<>(10);
        String message = switch (result) {
            case Success<Integer> s -> "Success: " + s.value;
            case Failure<Integer> f -> "Failure: " + f.error;
        };
        System.out.println(message);
    }
}在这个例子中,Result 是一个Sealed Class,它有两个子类 Success 和 Failure。模式匹配代码使用 switch 表达式来处理 Result 对象。编译器会检查 switch 表达式是否覆盖了 Result 的所有子类。如果遗漏了任何一个子类,编译器会发出警告或错误。
编译器的具体实现步骤:
- 模式匹配分析: 编译器分析 switch表达式或instanceof模式匹配中的类型模式。
- 子类集合检索: 编译器检索Sealed Class的 permits子句,获取所有子类的类型信息。
- 完备性检查: 编译器比较模式匹配中的类型模式和Sealed Class的子类集合。如果模式匹配没有覆盖所有子类,编译器会发出警告或错误。
- switch表达式: 如果- switch表达式没有- default分支,并且没有覆盖所有子类,编译器会报错。
- instanceof模式匹配: 如果- instanceof模式匹配没有覆盖所有子类,编译器可能会发出警告。
 
示例:遗漏子类的错误
sealed class Shape permits Circle, Rectangle {}
final class Circle extends Shape { double radius; }
final class Rectangle extends Shape { double width, height; }
public class Main {
  public static void main(String[] args) {
    Shape shape = new Circle();
    String description = switch (shape) {
      case Circle c -> "It's a circle";
      // Missing Rectangle case
    };
    System.out.println(description); // 编译器报错,提示需要default或者覆盖所有子类
  }
}处理 non-sealed 子类:
如果Sealed Class有一个 non-sealed 子类,编译器通常不会强制要求模式匹配完备性。因为 non-sealed 子类可以被任意类继承,编译器无法确定所有可能的子类。在这种情况下,通常需要一个 default 分支来处理未知的子类。
编译器实现的底层技术
编译器实现这些检查和验证通常涉及以下底层技术:
- 符号表: 编译器使用符号表来存储类、方法、字段等符号的信息。在处理Sealed Class时,编译器会将Sealed Class的子类信息存储在符号表中,以便进行后续的检查和验证。
- 类型系统: 编译器使用类型系统来跟踪类型信息,并进行类型检查。在处理Sealed Class时,编译器会使用类型系统来验证子类的类型是否正确,以及模式匹配是否完备。
- 控制流分析: 编译器使用控制流分析来分析程序的控制流,并检查模式匹配是否覆盖了所有可能的执行路径。
Sealed Class的优势
Sealed Class提供了以下优势:
- 增强的类型安全性: Sealed Class限制了类的继承,从而提供了更强的类型安全性。编译器可以在编译时检查类型错误,并防止运行时出现意外的类型转换。
- 更强大的模式匹配: Sealed Class可以用于模式匹配,并且编译器可以检查模式匹配是否完备。这使得代码更加简洁、可读,并且减少了错误。
- 更好的代码可维护性: Sealed Class使得代码更加易于理解和维护。通过明确声明允许继承的子类,可以减少代码的复杂性,并提高代码的可预测性。
代码案例:使用Sealed Class实现状态机
sealed interface State permits Initial, Running, Paused, Stopped {}
record Initial() implements State {}
record Running() implements State {}
record Paused() implements State {}
record Stopped() implements State {}
class StateMachine {
    private State currentState = new Initial();
    public void start() {
        currentState = switch (currentState) {
            case Initial i -> new Running();
            case Running r -> r;
            case Paused p -> new Running();
            case Stopped s -> new Running();
        };
        System.out.println("State changed to: " + currentState.getClass().getSimpleName());
    }
    public void pause() {
        currentState = switch (currentState) {
            case Initial i -> i;
            case Running r -> new Paused();
            case Paused p -> p;
            case Stopped s -> s;
        };
        System.out.println("State changed to: " + currentState.getClass().getSimpleName());
    }
    public void stop() {
        currentState = switch (currentState) {
            case Initial i -> new Stopped();
            case Running r -> new Stopped();
            case Paused p -> new Stopped();
            case Stopped s -> s;
        };
        System.out.println("State changed to: " + currentState.getClass().getSimpleName());
    }
    public State getCurrentState() {
        return currentState;
    }
    public static void main(String[] args) {
        StateMachine machine = new StateMachine();
        machine.start();
        machine.pause();
        machine.stop();
    }
}在这个例子中,State 是一个Sealed Interface,它定义了状态机的所有可能状态。 StateMachine 类使用 switch 表达式来处理状态转换。由于 State 是一个Sealed Interface,编译器可以检查 switch 表达式是否覆盖了所有可能的状态。
总结一下Sealed Class 的编译时检查
Sealed Class通过限制类的继承,提供了更强的类型安全性和模式匹配能力。编译器通过进行子类集合的完整性检查、子类类型的限制、同一模块或包的限制(Java 17及之前)以及模式匹配的完备性检查,来确保Sealed Class的正确使用。 理解编译器如何处理Sealed Class对于我们更好地利用这个特性至关重要。 Java 19 进一步放宽了限制,允许跨模块和包,提高了灵活性。