Кофе-брейк #143. Запечатанные (sealed) классы в Java 17. 4 способа реализации Singleton
Источник: Codippa В этой публикации мы рассмотрим запечатанные (sealed) классы — новую функцию, представленную в Java 17, а также способы их объявления и использования с примерами. Запечатанные классы впервые появились в Java 15 в качестве функции предварительного просмотра, а затем и в Java 16, в том же самом статусе. Полноценной эта функция стала с выходом Java 17 (JEP 409).
Что такое запечатанные классы?
final означает, что он не может быть далее подклассифицирован.
sealed означает, что нам нужно объявить дочерние классы с permits .
non-sealed означает, что здесь мы заканчиваем иерархию parent-child (родительский-дочерний).
Основные цели введения запечатанных классов:
До сих пор вы могли ограничить расширение класса только с помощью ключевого слова final . Запечатанный класс контролирует, какие классы могут его расширять, включая их в разрешенный список.
Также это позволяет классу контролировать, какие из них будут его дочерними классами.
Правила
Запечатанный класс должен определять классы, которые могут расширять его с помощью permits . Этого не требуется, если дочерние классы определены внутри родительского класса как внутренний класс.
Дочерний класс должен быть либо final , sealed либо non-sealed .
Разрешенный дочерний класс (permitted child class) должен расширять родительский запечатанный класс.
То есть если запечатанный класс A допускает класс B, то B должен расширить A.
Если запечатанный класс находится в модуле, то дочерние классы также должны быть в том же модуле или в том же пакете, если родительский запечатанный класс находится в безымянном модуле.
Только непосредственно разрешенные классы (directly permitted classes) могут расширять запечатанный класс. То есть, если A является запечатанным классом, который позволяет B расширять его, то B также является запечатанным классом, который разрешает C.
Что такое 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.
Kotlin. Изолированные (запечатанные) классы (sealed classes).
Изолированный класс — это еще одно новшество в языке Kotlin, которого не было в Java. Тем не менее, само по себе понятие в программировании не является новым — Kotlin позаимствовал его у других языков.
В официальной документации изолированному классу было дано такое определение: класс, который позволяет ограничить иерархию классов конкретным множеством подтипов, каждый из которых может определять собственные свойства и функции. На мой взгляд, формулировка не особо понятна.
Если говорить проще, то это абстрактный класс, который содержит в себе другие классы. По концепции очень похоже на enum , но с суперсилой. Выражена эта суперсила в том, что позволяет высвободиться от минусов enum . А именно:
- В enum каждое значение — это константа, которая существует в единственном экземпляре. Значение константы нельзя подстроить под конкретную ситуацию, потому что при изменении значения в одном месте, оно изменится везде. В изолированном же классе можно создать столько подклассов, сколько необходимо для покрытия каждой ситуации. Помимо этого, каждый подкласс может иметь несколько экземпляров, каждый из которых будет нести в себе свое собственное состояние.
- Каждое значение в enum должно содержать одинаковый набор свойств. Не получится какому-либо значению задать дополнительное свойство. Напротив, каждый подкласс изолированного класса имеет свой конструктор со своими индивидуальными свойствами.
Для определения изолированного класса используется ключевое слово sealed .
В данном примере класс MessageType является изолированным. У него есть два подкласса-наследника — Success() и Failure() , каждый из которых имеет индивидуальный набор свойств. Тут может возникнуть вопрос: как Success() и Failure() могут наследоваться от MessageType() , если он не отмечен ключевым словом open ? Всё просто: изолированный класс “открыт” для наследования по умолчанию, и дополнительно указывать слово open не требуется.
Также обратите внимание, что несмотря на то, что изолированный класс может иметь наследников, все они должны быть перечислены в одном с ним файле. Однако классы, которые расширяют наследников изолированного класса могут находиться где угодно.
Помимо этого, изолированный класс абстрактен по умолчанию и может содержать в себе абстрактные компоненты.
Изолированный класс можно использовать совместно с условным выражением when , при этом указывать ветку else не требуется.
На данный момент об изолированных классах сказать больше нечего. Поэтому резюмируем:
- Изолированные классы — это enum с суперсилой.
- У изолированного класса могут быть наследники, но все они должны находиться в одном файле с изолированным классом. Классы, которые расширяют наследников изолированного класса могут находиться где угодно.
- Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты.
- Конструктор изолированного класса всегда приватен, и это нельзя изменить.
- Изолированные классы нельзя инициализировать.
- Наследники изолированного класса могут быть классами любого типа: классом данных, объектом, обычным классом или даже другим изолированным классом.
Полезные ссылки
Sealed Classes — официальная документация.
Изолированные классы — перевод на русский.
Изолированные классы
Изолированные классы и интерфейсы позволяют выразить ограниченные иерархии классов, которые обеспечивают больший контроль над наследованием. Во время компиляции известны все прямые наследники изолированного класса. Никакие другие наследники не могут появиться после компиляции модуля с изолированным классом. Например, сторонние клиенты не могут расширить ваш изолированный класс в своем коде. Таким образом, каждый экземпляр изолированного класса имеет тип из ограниченного набора, который известен при компиляции этого класса.
То же самое справедливо для изолированных интерфейсов и их реализаций: новые реализации не могут появиться после компиляции модуля с изолированным интерфейсом.
Изолированные классы похожи на enum-классы: набор значений enum типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса может иметь несколько экземпляров, которые могут нести в себе какое-то состояние.
В качестве примера рассмотрим API библиотеки. Вероятно, он будет содержать классы ошибок, чтобы пользователи библиотеки могли обрабатывать возникающие ошибки. Если иерархия таких классов ошибок включает интерфейсы или абстрактные классы, видимые в общедоступном API, то ничто не препятствует их реализации или расширению в клиентском коде. Однако библиотека не знает об ошибках, объявленных за её пределами, поэтому не может обрабатывать их согласованно с помощью собственных классов. Благодаря изолированной иерархии классов ошибок авторы библиотек могут быть уверены, что им известны все возможные типы ошибок, и никакие другие не могут появиться позже.
Чтобы описать изолированный класс или интерфейс, укажите модификатор sealed перед его именем.
Сам по себе изолированный класс является абстрактным, он не может быть создан напрямую и может иметь абстрактные компоненты.
Конструкторы изолированных классов могут иметь одну из двух видимостей: protected (по умолчанию) или private .
Расположение прямых наследников
Прямые наследники изолированных классов и интерфейсов должны быть объявлены в том же пакете. Они могут быть верхнего уровня или вложены в любое количество других именованных классов, именованных интерфейсов или именованных объектов. Наследники могут иметь любую видимость, если они совместимы с обычными правилами наследования в Kotlin.
Наследники изолированных классов должны иметь правильные имена. Они не могут быть локальными или анонимными объектами.
`enum` classes can’t extend a sealed class (as well as any other class), but they can implement sealed interfaces. —>
Enum -классы не могут расширять изолированный класс (как и любой другой класс), но они могут реализовывать изолированные интерфейсы.
Эти ограничения не применяются к непрямым наследникам. Если прямой наследник изолированного класса не помечен как изолированный, он может быть расширен любыми способами, разрешенными его модификаторами.
Наследование в мультиплатформенных проектах
В мультиплатформенных проектах есть еще одно ограничение наследования: прямые наследники изолированных классов должны находиться в одном модуле. Это применимо к изолированным классам без модификаторов expect и actual .
Если изолированный класс объявлен как expected в общем модуле и имеет actual реализации в платформенном модуле, как ожидаемая, так и актуальные версии могут иметь наследников в своих модулях. Более того, если вы используете иерархическую структуру, вы можете создавать наследников в любом исходном наборе между expect и actual объявлениями.
Изолированные классы и выражение when
Ключевое преимущество от использования изолированных классов проявляется тогда, когда вы используете их в выражении when . Если возможно проверить, что выражение покрывает все случаи, то вам не нужно добавлять else . Однако, это работает только в том случае, если вы используете when как выражение (используя результат), а не как оператор.
`when` expressions on [`expect`](multiplatform-connect-to-apis.md) 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. —>
Выражение when в expect изолированных классах в общем коде многоплатформенных проектов по-прежнему требует ветки else . Это происходит потому, что наследники актуальных реализаций платформы не известны в общем коде.