1. 概述

Java SE 17 引入了密封类(Sealed Classes)这一特性(JEP 409)。

该特性旨在提供更细粒度的继承控制能力。通过密封机制,类和接口可以明确指定哪些子类或实现类是被允许的。

换句话说,一个类或接口现在可以声明只有特定的类才能继承它或实现它。这在领域建模和增强库安全性方面非常有用。

2. 动机

类层次结构能够通过继承实现代码复用,但这并不是类体系结构的唯一目的。

2.1. 领域建模的需求

有时候我们构建类层次结构是为了表达业务中已知的几种可能性。

举个例子,假设我们的业务只处理汽车(Car)和卡车(Truck),不涉及摩托车(Motorcycle)。那么在设计 Vehicle 抽象类时,我们希望只允许 CarTruck 继承它。这样可以确保该抽象类不会被滥用。

在这个场景中,我们更关注的是对已知子类的清晰建模,而不是防御未知的扩展。

在 Java 15 之前(密封类作为预览特性首次出现),Java 假设所有的类都应具备无限扩展的能力。每个类都可以被任意数量的子类继承。

2.2. 包私有方式的局限

早期版本中,Java 在继承控制方面的选择非常有限:

使用包私有的方式,我们无法做到让类对外可见但不允许扩展:

public class Vehicles {

    abstract static class Vehicle {

        private final String registrationNumber;

        public Vehicle(String registrationNumber) {
            this.registrationNumber = registrationNumber;
        }

        public String getRegistrationNumber() {
            return registrationNumber;
        }

    }

    public static final class Car extends Vehicle {

        private final int numberOfSeats;

        public Car(int numberOfSeats, String registrationNumber) {
            super(registrationNumber);
            this.numberOfSeats = numberOfSeats;
        }

        public int getNumberOfSeats() {
            return numberOfSeats;
        }

    }

    public static final class Truck extends Vehicle {

        private final int loadCapacity;

        public Truck(int loadCapacity, String registrationNumber) {
            super(registrationNumber);
            this.loadCapacity = loadCapacity;
        }

        public int getLoadCapacity() {
            return loadCapacity;
        }

    }

}

2.3. 超类可见但不可扩展

一个类与其子类一起设计时,应该能表达其预期用途,而不应对子类施加限制。此外,限制子类的扩展性不应影响超类的可访问性。

因此,密封类的核心动机是:让一个类广泛可用,但不能随意扩展。

3. 创建密封类和接口

密封机制引入了几个新的修饰符和关键字:sealednon-sealedpermits

3.1. 密封接口

要密封一个接口,可以在其声明前加上 sealed 关键字,并使用 permits 子句列出允许实现它的类:

public sealed interface Service permits Car, Truck {

    int getMaxServiceIntervalInMonths();

    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }

}

3.2. 密封类

密封类的定义方式类似。只需在类声明前加上 sealed,并在 extendsimplements 之后使用 permits 子句:

public abstract sealed class Vehicle permits Car, Truck {

    protected final String registrationNumber;

    public Vehicle(String registrationNumber) {
        this.registrationNumber = registrationNumber;
    }

    public String getRegistrationNumber() {
        return registrationNumber;
    }

}

✅ 被允许的子类必须显式声明修饰符。可以是 final 来禁止进一步扩展:

public final class Truck extends Vehicle implements Service {

    private final int loadCapacity;

    public Truck(int loadCapacity, String registrationNumber) {
        super(registrationNumber);
        this.loadCapacity = loadCapacity;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 18;
    }

}

也可以声明为 sealednon-sealed。如果声明为 non-sealed,则表示该类可以自由扩展:

public non-sealed class Car extends Vehicle implements Service {

    private final int numberOfSeats;

    public Car(int numberOfSeats, String registrationNumber) {
        super(registrationNumber);
        this.numberOfSeats = numberOfSeats;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 12;
    }

}

3.4. 限制条件

密封类对其允许的子类施加了三个重要约束:

  1. 所有允许的子类必须与密封类在同一个模块中。
  2. 每个允许的子类必须显式继承密封类。
  3. 每个允许的子类必须使用以下修饰符之一:finalsealednon-sealed

4. 使用方式

4.1. 传统判断方式

通过密封类,客户端代码可以清楚地知道所有允许的子类有哪些。

传统方式是使用 instanceof 和一系列 if-else 判断:

if (vehicle instanceof Car) {
    return ((Car) vehicle).getNumberOfSeats();
} else if (vehicle instanceof Truck) {
    return ((Truck) vehicle).getLoadCapacity();
} else {
    throw new RuntimeException("Unknown instance of Vehicle");
}

4.2. 模式匹配优化

借助 模式匹配,我们可以避免显式类型转换,但仍需使用 if-else 结构:

if (vehicle instanceof Car car) {
    return car.getNumberOfSeats();
} else if (vehicle instanceof Truck truck) {
    return truck.getLoadCapacity();
} else {
    throw new RuntimeException("Unknown instance of Vehicle");
}

⚠️ 使用 if-else 结构时,编译器无法确认是否覆盖了所有可能的子类,因此我们不得不抛出 RuntimeException

未来版本中(如 JEP 375),可以使用 switch 表达式替代 if-else。届时,编译器将能验证是否覆盖了所有允许的子类,从而不再需要 default 分支。

5. 兼容性分析

5.1. 与 Record 的兼容性

密封类与 Record 非常契合。由于 Record 是隐式 final 的,密封类结构会更加简洁。我们尝试将上面的例子改写为 Record:

public sealed interface Vehicle permits Car, Truck {

    String getRegistrationNumber();

}

public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

}

public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

}

5.2. 反射 API 支持

密封类在 反射 API 中也得到了支持,java.lang.Class 中新增了两个方法:

  • isSealed():如果类或接口是密封的,返回 true
  • getPermittedSubclasses():返回一个数组,表示所有允许的子类。

我们可以用这些方法来编写断言验证:

Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().getPermittedSubclasses())
  .contains(Class.forName(truck.getClass().getCanonicalName()));

6. 总结

✅ 在本文中,我们深入探讨了 Java SE 17 中引入的密封类与接口特性。我们介绍了其创建方式、使用方法、限制条件以及与其他语言特性的兼容性。

示例中包括:

  • 密封接口和密封类的创建
  • 使用密封类进行类型判断(含/不含模式匹配)
  • 密封类与 Record、反射 API 的结合使用

完整代码可从 GitHub 仓库 获取。


原始标题:Sealed Classes and Interfaces in Java 15