Запечатанный класс

Часто нам приходится представлять ограниченный набор возможностей: веб-запрос либо успешно выполняется, либо не выполняется, User может быть либо про-пользователем, либо обычным.
Чтобы смоделировать это, мы могли бы использовать enum , но это несет в себе ряд ограничений. Классы Enum допускают только один экземпляр каждого значения и не могут кодировать дополнительную информацию о каждом типе, например случай Error , имеющий соответствующее свойство Exception .
Вы можете использовать абстрактный класс и ряд расширений, но при этом теряется преимущество ограниченного набора типов, добавляемое перечислениями. Запечатанные классы берут лучшее из обоих миров: свободу представления абстрактных классов и ограниченный набор типов перечислений. Читайте дальше, чтобы узнать больше о запечатанных классах, или, если вы предпочитаете видео, посмотрите его здесь (англ):
Основы запечатанных классов
Как и абстрактные классы, запечатанные классы позволяют представлять иерархии. Дочерними классами могут быть классы любого типа: класс данных, объект, обычный класс или даже другой запечатанный класс. В отличие от абстрактных классов, вы должны определить эти иерархии в том же файле, где и вложенные.
Попытка расширить запечатанный класс за пределы файла, в котором он был определен, приводит к ошибке компиляции:
Забываешь про ветку?
Часто мы хотим обрабатывать все возможные типы:
Но что делать, если кто-то добавляет новый тип Result : InProgress :
Вместо того, чтобы полагаться на память или поиск средствами IDE, для гарантии того, что все использования when обрабатывают новый класс, компилятор может выдать нам ошибку, если ветвь не покрыта. when , как и оператор if , требует от нас лишь охватить все варианты (т.е. быть исчерпывающими), создавая ошибку компилятора, когда он используется в качестве выражения:
Выражение when должно быть исчерпывающим, поэтому добавьте необходимую ветвь “is InProgress” или какую-либо другую. Чтобы получить это замечательное преимущество, даже если мы используем when в качестве оператора, добавьте следующее свойство вспомогательного расширения:
Так что теперь, добавляя .exhaustive , если ветвь отсутствует, компилятор выдаст нам ту же ошибку, которую мы видели ранее.
Автозаполнение IDE
Поскольку известны все подтипы запечатанного класса, IDE может заполнить все возможные ветви оператора when за нас:
Эта функция действительно очень полезна при работе с более сложными иерархиями запечатанных классов, поскольку IDE может распознавать все ветви:
Это тип функциональности, который не может быть реализован с абстрактными классами, поскольку в данном случае компилятор не знает иерархию наследования, следовательно IDE не может генерировать ветви.
Под капотом
Так что же заставляет запечатанные классы вести себя именно так? Давайте посмотрим, что происходит в декомпилированном коде Java:
Метаданные запечатанного класса сохраняют список дочерних классов, позволяя компилятору использовать эту информацию там, где это необходимо.
Result реализован в виде абстрактного класса с двумя конструкторами:
- Приватный конструктор по умолчанию
- Синтетический конструктор, который может использоваться только компилятором Kotlin
Таким образом, это означает, что ни один другой класс не может непосредственно вызвать конструктор. Если мы посмотрим на декомпилированный код класса Success, то увидим, что он вызывает синтетический конструктор:
Начните использовать запечатанные классы для моделирования ограниченных иерархий классов, позволяя компилятору и IDE помочь вам избежать ошибок согласования типов.
Sealed classes
Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear after a module with the sealed class is compiled. For example, third-party clients can’t extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled.
The same works for sealed interfaces and their implementations: once a module with a sealed interface is compiled, no new implementations can appear.
In some sense, sealed classes are similar to enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances, each with its own state.
As an example, consider a library’s API. It’s likely to contain error classes to let the library users handle errors that it can throw. If the hierarchy of such error classes includes interfaces or abstract classes visible in the public API, then nothing prevents implementing or extending them in the client code. However, the library doesn’t know about errors declared outside it, so it can’t treat them consistently with its own classes. With a sealed hierarchy of error classes, library authors can be sure that they know all possible error types and no other ones can appear later.
To declare a sealed class or interface, put the sealed modifier before its name:
A sealed class is abstract by itself, it cannot be instantiated directly and can have abstract members.
Constructors of sealed classes can have one of two visibilities: protected (by default) or private :
Location of direct subclasses
Direct subclasses of sealed classes and interfaces must be declared in the same package. They may be top-level or nested inside any number of other named classes, named interfaces, or named objects. Subclasses can have any visibility as long as they are compatible with normal inheritance rules in Kotlin.
Subclasses of sealed classes must have a proper qualified name. They can’t be local nor anonymous objects.
enum classes can’t extend a sealed class (as well as any other class), but they can implement sealed interfaces.
These restrictions don’t apply to indirect subclasses. If a direct subclass of a sealed class is not marked as sealed, it can be extended in any way that its modifiers allow:
Inheritance in multiplatform projects
There is one more inheritance restriction in multiplatform projects: direct subclasses of sealed classes must reside in the same source set. It applies to sealed classes without the expect and actual modifiers.
If a sealed class is declared as expect in a common source set and have actual implementations in platform source sets, both expect and actual versions can have subclasses in their source sets. Moreover, if you use a hierarchical structure, you can create subclasses in any source set between the expect and actual declarations.
Sealed classes and when expression
The key benefit of using sealed classes comes into play when you use them in a when expression. If it’s possible to verify that the statement covers all cases, you don’t need to add an else clause to the statement. However, this works only if you use when as an expression (using the result) and not as a statement:
when expressions on expect sealed classes in the common code of multiplatform projects still require an else branch. This happens because subclasses of actual platform implementations aren’t known in the common code.
Что такое sealed class
Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.
This is a preview feature, which is a feature whose design, specification, and implementation are complete, but is not permanent, which means that the feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Features.
For background information about sealed classes and interfaces, see JEP 397.
One of the primary purposes of inheritance is code reuse: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug) them yourself.
However, what if you want to model the various possibilities that exist in a domain by defining its entities and determining how these entities should relate to each other? For example, you’re working on a graphics library. You want to determine how your library should handle common geometric primitives like circles and squares. You’ve created a Shape class that these geometric primitives can extend. However, you’re not interested in allowing any arbitrary class to extend Shape ; you don’t want clients of your library declaring any further primitives. By sealing a class, you can specify which classes are permitted to extend it and prevent any other arbitrary class from doing so.
Defining Sealed Classes
To seal a class, add the sealed modifier to its declaration. Then, after any extends and implements clauses, add the permits clause. This clause specifies the classes that may extend the sealed class.
For example, the following declaration of Shape specifies three permitted subclasses, Circle , Square , and Rectangle :
Figure 3-1 Shape.java
Define the following three permitted subclasses, Circle , Square , and Rectangle , in the same module or in the same package as the sealed class:
Figure 3-2 Circle.java
Figure 3-3 Square.java
Figure 3-4 Rectangle.java
Rectangle has a further subclass, FilledRectangle :
Figure 3-5 FilledRectangle.java
Alternatively, you can define permitted subclasses in the same file as the sealed class. If you do so, then you can omit the permits clause:
Constraints on Permitted Subclasses
Permitted subclasses have the following constraints:
They must be accessible by the sealed class at compile time.
For example, to compile Shape.java , the compiler must be able to access all of the permitted classes of Shape : Circle.java , Square.java , and Rectangle.java . In addition, because Rectangle is a sealed class, the compiler also needs access to FilledRectangle.java .
They must directly extend the sealed class.
They must have exactly one of the following modifiers to describe how it continues the sealing initiated by its superclass:
final : Cannot be extended further
sealed : Can only be extended by its permitted subclasses
non-sealed : Can be extended by unknown subclasses; a sealed class cannot prevent its permitted subclasses from doing this
For example, the permitted subclasses of Shape demonstrate each of these three modifiers: Circle is final while Rectangle is sealed and Square is non-sealed .
They must be in the same module as the sealed class (if the sealed class is in a named module) or in the same package (if the sealed class is in the unnamed module, as in the Shape.java example).
For example, in the following declaration of com.example.graphics.Shape , its permitted subclasses are all in different packages. This example will compile only if Shape and all of its permitted subclasses are in the same named module.
Defining Sealed Interfaces
Like sealed classes, to seal an interface, add the sealed modifier to its declaration. Then, after any extends clause, add the permits clause, which specifies the classes that can implement the sealed interface and the interfaces that can extend the sealed interface.
The following example declares a sealed interface named Expr . Only the classes ConstantExpr , PlusExpr , TimesExpr , and NegExpr may implement it:
Record Classes as Permitted Subclasses
You can name a record class in the permits clause of a sealed class or interface. See Record Classes for more information.
Record classes are implicitly final , so you can implement the previous example with record classes instead of ordinary classes:
Narrowing Reference Conversion and Disjoint Types
Narrowing reference conversion is one of the conversions used in type checking cast expressions. It enables an expression of a reference type S to be treated as an expression of a different reference type T , where S is not a subtype of T . A narrowing reference conversion may require a test at run time to validate that a value of type S is a legitimate value of type T . However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.
Consider the following example:
The cast expression Polygon p = (Polygon) r is permitted because it’s possible that the Rectangle value r could be of type Polygon ; Rectangle is a subtype of Polygon . However, consider this example:
Even though the class Triangle and the interface Polygon are unrelated, the cast expression Polygon p = (Polygon) t is also permitted because at run time these types could be related. A developer could declare the following class:
However, there are cases where the compiler can deduce that there are no values (other than the null reference) shared between two types; these types are considered disjoint . For example:
Because the class UtahTeapot is final , it’s impossible for a class to be a descendant of both Polygon and UtahTeapot . Therefore, Polygon and UtahTeapot are disjoint, and the cast statement Polygon p = (Polygon) u isn’t permitted.
The compiler has been enhanced to navigate any sealed hierarchy to check if your cast statements are permitted. For example:
The first cast statement UtahTeapot u = (UtahTeapot) s isn’t permitted; a Shape can only be a Polygon because Shape is sealed . However, as Polygon is non-sealed , it can be extended. However, no potential subtype of Polygon can extend UtahTeapot as UtahTeapot is final . Therefore, it’s impossible for a Shape to be a UtahTeapot .
In contrast, the second cast statement Ring r = (Ring) s is permitted; it’s possible for a Shape to be a Ring because Ring is not a final class.