sealed-классы (или запечатанные классы) и sealed-интерфейсы (или запечатанные интерфейсы) в языке Java управляют тем, какие именно классы могут расширять данный класс или интерфейс.
Эта функция вносит уровень контроля и предсказуемости в иерархии наследования, который ранее был недоступен, кроме как через менее гибкие механизмы, такие как final или ограничения доступа.
Прежде всего встает вопрос: зачем же нужны sealed-классы? До появления sealed-классов в Java иерархия классов могла быть либо полностью открытой для расширения
(любой класс мог наследоваться), либо быть полностью закрытым для наследования с помощью модификатора final. Sealed-классы предлагают золотую середину. Они позволяют контролировать расширяемость - явно указать, какие классы могут быть прямыми подклассами,
предотвращая неожиданное и несанкционированное наследование. Кроме того, они предоставляют эффективный способ моделирования ADT (Алгебраических типов данных) в Java, где базовый тип имеет фиксированный набор вариантов.
Чтобы объявить класс или интерфейс как sealed, используются два ключевых слова: sealed и permits:
sealed class имя_класса permits подкласс_1, подкласс_2, ююю подкласс_N {
// содержимое класса
}
Ключевое слово sealed применяется к суперклассу или интерфейсу для объявления его запечатанным.
После имени класса указывается ключевое слово permits, за которым следует список классов, которым разрешено быть прямыми наследниками. Далее будем также называть эти классы разрешенными классами.
Пример:
sealed class Operation
permits Sum, Subtract, Multiply
{ }
В данном случае мы разрешаем классам Sum, Subtract и Multiply быть наследниками класса Operation. Однако для всех остальных классов установлен запроет на наследование. Например, в следующем случае мы столкнемся с ошибкой на этапе компиляции:
sealed class Operation
permits Sum, Subtract, Multiply
{ }
class Divide extends Operation // Ошибка - для класса Divide не разрешено наследование
{ }
Классы, перечисленные после слова permits, также должны следовать определенным правилам. Прежде всего должны соблюдаться правила расположения классов:
Разрешенные подклассы sealed-класса должны быть доступны. Они не могут быть приватными классами, вложенными в другой класс, или классами, которые доступны только на уровне другого пакета.
Разрешенные публичные подклассы должны находиться в том же пакете, что и sealed-класс. Если применяются модули, то разрешенные подклассы должны находиться в том же модуле, что и sealed-класс.
sealed-класс может быть объявлен без оператора permits. Тогда все его прямые подклассы должны быть объявлены в одном файле.
Кроме того, разрешенные классы должны явно объявлять, как они продолжают (или прекращают) иерархию запечатанного класса. Для этого к разрешенному классу применяется один из следующих трех модификаторов:
final: запрещает любое дальнейшее наследование, завершая ветвь иерархии:
sealed class Operation
permits Sum, Subtract, Multiply { }
final class Sum extends Operation { }
final class Subtract extends Operation {}
final class Multiply extends Operation {}
sealed: разрешает ограниченное дальнейшее наследование, но только тем классам, которые идут после permits:
sealed class Operation permits UnaryOp, BinaryOp{}
final class UnaryOp extends Operation { }
// разрешаем дальнейшее наследование, но только для классов Sum, Subtract, Multiply
sealed class BinaryOp extends Operation
permits Sum, Subtract, Multiply { }
final class Sum extends BinaryOp {}
final class Subtract extends BinaryOp {}
final class Multiply extends BinaryOp {}
non-sealed: открывает этот подкласс для расширения любыми классами, как обычный класс Java, продолжая ветвь иерархии
sealed class Operation permits UnaryOp, BinaryOp{}
final class UnaryOp extends Operation { }
// открываем для наследования произвольным классами
non-sealed class BinaryOp extends Operation {}
class Sum extends BinaryOp {}
class Subtract extends BinaryOp {}
class Multiply extends BinaryOp {}
Рассмотрим простейший пример с sealed-классами:
class Program{
public static void main(String[] args) {
Operation add = new Sum(5, 4);
Operation sub = new Subtract(5, 4);
System.out.println(add.execute()); // 9
System.out.println(sub.execute()); // 1
}
}
sealed abstract class Operation
permits Sum, Subtract
{
int op1;
int op2;
Operation(int op1, int op2){
this.op1 = op1;
this.op2 = op2;
}
abstract int execute();
}
final class Sum extends Operation{
Sum(int op1, int op2){ super(op1, op2); }
@Override int execute() { return op1 + op2;}
}
final class Subtract extends Operation{
Subtract(int op1, int op2){ super(op1, op2); }
@Override int execute() { return op1 - op2;}
}
Здесь абстрактный класс Operation определен как запечатанный sealed-класс. Он представляет арифметическую операцию, которая хранит два операнда в виде переменных op1 и op2 и определяет абстрактный
метод execute() для выполнения этой операции. И этот класс могут наследовать только классы Sum и Subtract.
В классах Sum и Subtract соответствующим образом реализуем метод execute() для выполнения соответствующей операции. Причем стоит отметить, что наследование от этих классов запрещено.
Рассмотрим более расширенный пример:
class Program{
public static void main(String[] args) {
Operation add = new Sum(5, 4);
Operation sub = new Subtract(5, 4);
Operation neg = new Negation(5);
Operation if1 = new IfOperation(true, 3, 7);
Operation if2 = new IfOperation(false, 3, 7);
System.out.println(add.execute()); // 9
System.out.println(sub.execute()); // 1
System.out.println(neg.execute()); // -5
System.out.println(if1.execute()); // 3
System.out.println(if2.execute()); // 7
}
}
sealed abstract class Operation permits BinaryOperation, IfOperation{
abstract int execute();
}
// запрещаем дальнейшее наследование
// операция отрицания
final class IfOperation extends Operation {
boolean condition;
int op1;
int op2;
IfOperation(boolean cond, int op1, int op2){
this.condition = cond;
this.op1 = op1;
this.op2 = op2;
}
@Override int execute() { return condition ? op1 : op2; }
}
// разрешаем дальнейшее наследование для типов Sum, Subtract
sealed abstract class BinaryOperation extends Operation
permits Sum, Subtract
{
int op1;
int op2;
BinaryOperation(int op1, int op2){
this.op1 = op1;
this.op2 = op2;
}
}
final class Sum extends BinaryOperation{
Sum(int op1, int op2){ super(op1, op2); }
@Override int execute() { return op1 + op2;}
}
non-sealed class Subtract extends BinaryOperation{
Subtract(int op1, int op2){ super(op1, op2); }
@Override int execute() { return op1 - op2;}
}
class Negation extends Subtract{
Negation(int op){ super(0, op); } // эквивалентно варажению 0 - op
}
Здесь у нас есть единый sealed-класс, который представляет операцию - Operation. От него могут наследоваться классы BinaryOperation (операция с двумя операндами) и IfOperation (операция с тремя операндами)
Класс IfOperation определен как final и запрещает дальнейшее наследование, он реализует своего рода тернарную операцию.
А вот класс BinaryOperation позволяет унаследоваться двум классам - Sum и Subtract. Причем Subtract определен с модификатором non-sealed, что позволяет наследоваться от него произвольным классам. Как например, классу
Negation, который реализует операцию унарного минуса.
Концепция sealed-типов также применима к интерфейсам. Sealed-интерфейс может быть реализован или расширен только теми классами или интерфейсами, которые указаны в его предложении
>permits. Например:
public sealed interface Shape permits Rectangle, Circle {
// Методы интерфейса
}