Sealed Classes and Interfaces in Java – Java中密封的类和接口

最后修改: 2020年 11月 11日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The release of Java SE 17 introduces sealed classes (JEP 409).

Java SE 17的发布引入了密封类(JEP 409)。

This feature is about enabling more fine-grained inheritance control in Java. Sealing allows classes and interfaces to define their permitted subtypes.

该功能是为了在Java中实现更细化的继承控制。密封性允许类和接口定义其允许的子类型。

In other words, a class or an interface can now define which classes can implement or extend it. It is a useful feature for domain modeling and increasing the security of libraries.

换句话说,一个类或一个接口现在可以定义哪些类可以实现或扩展它。这对于领域建模和增加库的安全性是一个有用的功能。

2. Motivation

2.动机

A class hierarchy enables us to reuse code via inheritance. However, the class hierarchy can also have other purposes. Code reuse is great but is not always our primary goal.

类的层次结构使我们能够通过继承来重复使用代码。然而,类的层次结构也可以有其他目的。代码重用很好,但并不总是我们的首要目标。

2.1. Modeling Possibilities

2.1.建模的可能性

An alternative purpose of a class hierarchy can be to model various possibilities that exist in a domain.

类层次结构的另一个目的可以是对一个领域中存在的各种可能性进行建模。

As an example, imagine a business domain that only works with cars and trucks, not motorcycles. When creating the Vehicle abstract class in Java, we should be able to allow only Car and Truck classes to extend it. In that way, we want to ensure that there will be no misuse of the Vehicle abstract class within our domain.

举个例子,设想一个只与汽车和卡车打交道的商业领域,而不是摩托车。在Java中创建Vehicle抽象类时,我们应该只允许CarTruck类来扩展它。通过这种方式,我们要确保在我们的领域内不会出现滥用Vehicle抽象类的情况。

In this example, we are more interested in the clarity of code handling known subclasses than defending against all unknown subclasses.

在这个例子中,我们更关心的是处理已知子类的代码的清晰度,而不是对所有未知子类的防御

Before version 15 (in which sealed classes were introduced as a preview), Java assumed that code reuse is always a goal. Every class was extendable by any number of subclasses.

在第15版之前(密封类作为预览版被引入),Java认为代码重用总是一个目标。每个类都可以由任何数量的子类来扩展。

2.2. The Package-Private Approach

2.2.打包–私营的方法

In earlier versions, Java provided limited options in the area of inheritance control.

在早期版本中,Java在继承控制方面提供了有限的选择。

A final class can have no subclasses. A package-private class can only have subclasses in the same package.

一个final类不能有任何子类。一个package-private类只能在同一个包中拥有子类。

Using the package-private approach, users cannot access the abstract class without also allowing them to extend it:

使用包-私有的方法,用户不能访问抽象类,同时也不允许他们扩展它。

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. Superclass Accessible, Not Extensible

2.3.超类可访问,不可扩展

A superclass that is developed with a set of its subclasses should be able to document its intended usage, not constrain its subclasses. Also, having restricted subclasses should not limit the accessibility of its superclass.

一个与它的子类集合一起开发的超类应该能够记录它的预期用途,而不是限制它的子类。同时,拥有受限制的子类不应该限制其超类的可访问性。

Thus, the main motivation behind sealed classes is to have the possibility for a superclass to be widely accessible but not widely extensible.

因此,密封类背后的主要动机是让超类有可能被广泛地访问,但不可能被广泛地扩展。

3. Creation

3.创作

The sealed feature introduces a couple of new modifiers and clauses in Java: sealed, non-sealed, and permits.

密封功能在Java中引入了几个新的修饰语和子句。密封的,非密封的,许可

3.1. Sealed Interfaces

3.1.密封的接口

To seal an interface, we can apply the sealed modifier to its declaration. The permits clause then specifies the classes that are permitted to implement the sealed interface:

为了密封一个接口,我们可以在其声明中应用sealed修饰符。然后,permits子句指定了允许实现该密封接口的类。

public sealed interface Service permits Car, Truck {

    int getMaxServiceIntervalInMonths();

    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }

}

3.2. Sealed Classes

3.2.封闭类

Similar to interfaces, we can seal classes by applying the same sealed modifier. The permits clause should be defined after any extends or implements clauses:

与接口类似,我们可以通过应用相同的sealed修饰语来封印类。permits子句应该定义在任何extendsimplements子句之后。

public abstract sealed class Vehicle permits Car, Truck {

    protected final String registrationNumber;

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

    public String getRegistrationNumber() {
        return registrationNumber;
    }

}

A permitted subclass must define a modifier. It may be declared final to prevent any further extensions:

一个允许的子类必须定义一个修改器。它可以被宣布为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;
    }

}

A permitted subclass may also be declared sealed. However, if we declare it non-sealed, then it is open for extension:

一个允许的子类也可以被声明为密封的。然而,如果我们声明它是非密封的,那么它是可以被扩展的。

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. Constraints

3.4.限制条件

A sealed class imposes three important constraints on its permitted subclasses:

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

  1. All permitted subclasses must belong to the same module as the sealed class.
  2. Every permitted subclass must explicitly extend the sealed class.
  3. Every permitted subclass must define a modifier: final, sealed, or non-sealed.

4. Usage

4.使用情况

4.1. The Traditional Way

4.1.传统方式

When sealing a class, we enable the client code to reason clearly about all permitted subclasses.

当密封一个类时,我们使客户代码能够清楚地推理出所有允许的子类。

The traditional way to reason about subclass is using a set of if-else statements and instanceof checks:

推理子类的传统方法是使用一组if-else语句和instanceof检查。

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. Pattern Matching

4.2.模式匹配

By applying pattern matching, we can avoid the additional class cast, but we still need a set of if-else statements:

通过应用模式匹配,我们可以避免额外的类铸造,但我们仍然需要一组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");
}

Using if-else makes it difficult for the compiler to determine that we covered all permitted subclasses. For that reason, we are throwing a RuntimeException.

使用if-else会使编译器难以确定我们是否涵盖了所有允许的子类。出于这个原因,我们要抛出一个RuntimeException

In future versions of Java, the client code will be able to use a switch statement instead of if-else (JEP 375).

在未来的Java版本中,客户端代码将能够使用switch 语句而不是if-elseJEP 375)。

By using type test patterns, the compiler will be able to check that every permitted subclass is covered. Thus, there will be no more need for a default clause/case.

通过使用类型测试模式,编译器将能够检查每个允许的子类是否被覆盖。因此,将不再需要default子句/案例。

4. Compatibility

4.兼容性

Let’s now take a look at the compatibility of sealed classes with other Java language features like records and the reflection API.

现在让我们来看看密封类与其他Java语言特性的兼容性,如记录和反射API。

4.1. Records

4.1.记录

Sealed classes work very well with records. Since records are implicitly final, the sealed hierarchy is even more concise. Let’s try to rewrite our class example using records:

密封类在记录方面的工作非常好。由于记录是隐含的最终的,所以密封的层次结构更加简明。让我们试着用记录来重写我们的类例。

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;
    }

}

4.2. Reflection

4.2.反射

Sealed classes are also supported by the reflection API, where two public methods have been added to the java.lang.Class:

reflection API也支持密封类,在java.lang.Class:中添加了两个公共方法。

  • The isSealed method returns true if the given class or interface is sealed.
  • Method getPermittedSubclasses returns an array of objects representing all the permitted subclasses.

We can make use of these methods to create assertions that are based on our example:

我们可以利用这些方法来创建以我们的例子为基础的断言。

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

5. Conclusion

5.总结

In this article, we explored sealed classes and interfaces, a new feature in Java SE 17. We covered the creation and usage of sealed classes and interfaces, as well as their constraints and compatibility with other language features.

在这篇文章中,我们探讨了密封类和接口,这是Java SE 17的一个新特性。我们介绍了密封类和接口的创建和使用,以及它们的约束和与其他语言特性的兼容性。

In the examples, we covered the creation of a sealed interface and a sealed class, the usage of the sealed class (with and without pattern matching), and sealed classes compatibility with records and the reflection API.

在例子中,我们涵盖了一个密封接口和一个密封类的创建,密封类的使用(有和没有模式匹配),以及密封类与记录和反射 API 的兼容性。

As always, the complete source code is available over on GitHub.

一如既往,完整的源代码可在GitHub上获得