Java Sealed Classes
last modified May 28, 2025
This article explains sealed classes in Java, a feature introduced in Java 17 to enforce controlled inheritance hierarchies. Sealed classes allow developers to limit which subclasses can extend a given class, ensuring better design enforcement and preventing unintended inheritance.
Sealed classes provide explicit control over class extension, allowing only specified subtypes to inherit from them. This approach enhances domain modeling, enforcing constraints on class hierarchies.
Key characteristics of sealed classes:
- Declared with the
sealed
modifier to indicate restricted inheritance. - Specify permitted subclasses using the
permits
keyword. - Permitted subtypes must reside in the same module or package as the sealed class.
- Subtypes must explicitly extend the sealed class and declare their inheritance type.
Basic Syntax
A sealed class defines a fixed set of subclasses that can extend it, as shown below. This approach ensures that only the explicitly permitted types can participate in the inheritance hierarchy, providing greater control and predictability in your codebase.
public sealed class Vehicle permits Car, Truck { // class members }
Permitted subtypes must specify one of the following inheritance types:
final
- Prevents any further subclassing.sealed
- Allows subclassing but only for specified subtypes.non-sealed
- Removes all restrictions, allowing unrestricted inheritance.
Sealed classes provide several advantages:
- Encapsulation of business logic - Prevents unwanted subclassing.
- Enhances security - Protects APIs by restricting subclass modifications.
- Improved code maintainability - Reduces complexity by defining fixed hierarchies.
- Better compiler checks - Helps catch unintended inheritance issues early.
Simple Sealed Class Example
Basic sealed class with implementations. This example demonstrates how to declare a sealed class and its permitted subclasses, illustrating the enforcement of controlled inheritance in practice.
sealed abstract class Shape permits Circle, Rectangle { abstract double area(); } final class Circle extends Shape { private final double radius; Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } } final class Rectangle extends Shape { private final double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } @Override double area() { return width * height; } } void main() { Shape circle = new Circle(5.0); Shape rectangle = new Rectangle(4.0, 6.0); System.out.println("Circle area: " + circle.area()); System.out.println("Rectangle area: " + rectangle.area()); }
In this example, we define a sealed abstract class Shape
that permits
only Circle
and Rectangle
as its subclasses. The
Shape
class has an abstract method area
, which must be
implemented by its subclasses. The Circle
and Rectangle
classes are marked as final
, meaning they cannot be further
extended. This structure ensures that the Shape
hierarchy remains
controlled and predictable, allowing only the specified subclasses to
participate in the inheritance chain.
Sealed Class Hierarchy
Creating a hierarchy of sealed classes. This section shows how sealed classes can be extended across multiple levels, allowing for complex but well-defined inheritance structures.
sealed class Vehicle permits Car, Truck { protected String manufacturer; Vehicle(String manufacturer) { this.manufacturer = manufacturer; } } sealed class Car extends Vehicle permits ElectricCar { Car(String manufacturer) { super(manufacturer); } } final class ElectricCar extends Car { ElectricCar(String manufacturer) { super(manufacturer); } } final class Truck extends Vehicle { Truck(String manufacturer) { super(manufacturer); } } void main() { Vehicle tesla = new ElectricCar("Tesla"); Vehicle ford = new Truck("Ford"); System.out.println(tesla.manufacturer); System.out.println(ford.manufacturer); }
This example demonstrates a sealed class Vehicle
with two permitted
subclasses: Car
and Truck
. The Car
class is
sealed and permits only ElectricCar
as a subclass. This structure
allows for a controlled hierarchy where only specified subclasses can extend the
Vehicle
class, ensuring that the inheritance remains predictable
and manageable.
Pattern Matching with Sealed Classes
Using pattern matching with sealed classes. Pattern matching with sealed classes enables concise and exhaustive handling of all possible subtypes, making your code safer and easier to maintain.
sealed class Expr permits Constant, Add, Subtract { abstract int eval(); } final class Constant extends Expr { private final int value; Constant(int value) { this.value = value; } @Override int eval() { return value; } } final class Add extends Expr { private final Expr left, right; Add(Expr left, Expr right) { this.left = left; this.right = right; } @Override int eval() { return left.eval() + right.eval(); } } void main() { Expr expr = new Add(new Constant(5), new Constant(3)); System.out.println("Result: " + eval(expr)); } int eval(Expr e) { return switch (e) { case Constant c -> c.eval(); case Add a -> eval(a.left) + eval(a.right); case Subtract s -> eval(s.left) - eval(s.right); }; }
Sealed classes enable exhaustive pattern matching since all subtypes are known. This feature allows the compiler to verify that all cases are handled, reducing the risk of runtime errors due to unhandled subclasses.
Sealed Interfaces
Java also allows interfaces to be sealed, providing the same control over which classes or interfaces can implement or extend them. Sealed interfaces are useful for modeling hierarchies where both classes and interfaces need restricted extension, ensuring a fixed set of permitted implementors. This mechanism helps maintain strict boundaries in your type system and supports robust domain modeling.
sealed interface Shape permits Circle, Rectangle, Polygon { double area(); } final class Circle implements Shape { private final double radius; Circle(double radius) { this.radius = radius; } public double area() { return Math.PI * radius * radius; } } final class Rectangle implements Shape { private final double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } public double area() { return width * height; } } non-sealed class Polygon implements Shape { private final double area; Polygon(double area) { this.area = area; } public double area() { return area; } } void main() { Shape c = new Circle(3); Shape r = new Rectangle(4, 5); Shape p = new Polygon(12); System.out.println("Circle area: " + c.area()); System.out.println("Rectangle area: " + r.area()); System.out.println("Polygon area: " + p.area()); }
This example demonstrates a sealed interface Shape
with three
permitted implementors: a final Circle
, a final
Rectangle
, and a non-sealed Polygon
that can be
further extended. Sealed interfaces provide the same benefits as sealed classes,
including exhaustive pattern matching and controlled extension. By sealing
interfaces, you can enforce architectural constraints and improve code
reliability.
The non-sealed Keyword
The non-sealed
keyword in Java allows a permitted subclass of a
sealed class or interface to opt out of sealing, making it open for further
extension. This means that while the parent restricts which classes can extend
it, a non-sealed
subclass can be freely extended by any other
class, removing the restriction for its own hierarchy. This flexibility is
useful when you want to allow future expansion in certain branches of your class
hierarchy while keeping others tightly controlled.
sealed class Animal permits Dog, Cat, WildAnimal {} final class Dog extends Animal {} non-sealed class Cat extends Animal {} class PersianCat extends Cat {} class SiameseCat extends Cat {} non-sealed class WildAnimal extends Animal {} void main() { Cat genericCat = new Cat(); PersianCat persian = new PersianCat(); SiameseCat siamese = new SiameseCat(); System.out.println("Cat: " + genericCat.getClass().getSimpleName()); System.out.println("PersianCat: " + persian.getClass().getSimpleName()); System.out.println("SiameseCat: " + siamese.getClass().getSimpleName()); }
In this example, Animal
is a sealed class that permits
Dog
, Cat
, and WildAnimal
as subclasses.
Dog
is final and cannot be extended further. Cat
is
marked non-sealed
, so it can be extended by any class, such as
PersianCat
and SiameseCat
. This demonstrates how the
non-sealed
keyword reopens the inheritance hierarchy for specific
subclasses, allowing for greater flexibility where needed.
The WildAnimal
class is also marked non-sealed
,
allowing it to be extended freely. This provides flexibility in the hierarchy
while still maintaining control over the base class Animal
. By
selectively using non-sealed
, you can balance strictness and
extensibility in your class designs.
When to Use Sealed Classes
Appropriate use cases include scenarios where you need to model a fixed set of implementations, define domain models with known variants, restrict API implementations, or leverage pattern matching for exhaustive handling of all possible subtypes. Sealed classes are particularly beneficial in these contexts because they provide compile-time guarantees about the structure of your type hierarchies.
Benefits of using sealed classes include better domain modeling, improved API security, support for exhaustive pattern matching, and more maintainable code. By restricting inheritance, you can prevent unintended extensions and make your codebase easier to understand and maintain.
Source
In this article we explored Java sealed classes for controlled inheritance. They make code more secure and maintainable through restricted hierarchies.
Author
List all Java tutorials.