Java: Sealed Classes

24 Jul 2026 6 min read
1
Sealed classes allow developers to precisely control which classes or interfaces are permitted to extend or implement a type.

Unlike ordinary inheritance, where any class can create a subclass, a sealed class explicitly defines its allowed subclasses, resulting in a restricted and well-defined type hierarchy.

Sealed classes were introduced as a preview feature in Java 15 and became a standard language feature in Java 17.

Sealed Classes

A sealed class is declared using the sealed modifier together with a permits clause.
public sealed class Shape permits Circle, Rectangle, Triangle {
}
Only the classes listed in the permits clause are allowed to extend Shape. Any other class attempting to inherit from it results in a compilation error.

Every class that directly extends a sealed class must explicitly declare one of three modifiers: final, sealed, or non-sealed.

Final Subclass

A final subclass terminates the inheritance hierarchy.
public sealed class Shape permits Circle {
}

public final class Circle extends Shape {
}
No class can extend Circle. This is the most common choice when the hierarchy naturally ends with the subclass.

Sealed Subclass

A permitted subclass can itself be declared as sealed, creating another controlled level in the hierarchy.
public sealed class Shape permits Polygon {
}

public sealed class Polygon extends Shape permits Triangle, Rectangle {
}

public final class Triangle extends Polygon {
}

public final class Rectangle extends Polygon {
}
Each sealed subclass must specify its own permitted subclasses.

Non-Sealed Subclass

A non-sealed subclass removes all inheritance restrictions from that point onward.
public sealed class Shape permits Polygon {
}

public non-sealed class Polygon extends Shape {
}
Now any class may extend Polygon.
public class Triangle extends Polygon {
}

public class Rectangle extends Polygon {
}

public class Hexagon extends Polygon {
}
Using non-sealed effectively ends the restrictions imposed by the original sealed class and returns to ordinary inheritance.

The compiler also validates the hierarchy in both directions. If a class is listed in the permits clause but does not directly extend the sealed class, or if a direct subclass is omitted from the permits list, the compiler reports an error.

This bidirectional verification ensures that the hierarchy remains complete, explicit, and consistent.

For example:
public sealed class Shape permits Triangle {
}

public final class Triangle {
}
Compilation error:
Invalid 'permits' clause: 'Triangle' must directly extend 'Shape'.
Objects are created exactly like ordinary classes.
Shape shape = new Circle();
Sealed classes are useful whenever the set of valid subclasses is known in advance.

Sealed classes are commonly used to model fixed domain hierarchies, represent finite state machines, define Abstract Syntax Trees (ASTs), and support exhaustive pattern matching with switch expressions.

They are also useful for designing APIs that restrict inheritance, preserving invariants and preventing unauthorized subclassing.

Sealed Interfaces

The sealed modifier is not limited to classes. Interfaces can also be declared as sealed, allowing developers to explicitly control which classes or interfaces may implement or extend them.

This is particularly useful when defining a fixed set of implementations for an API while preventing unauthorized implementations.

For example,
public sealed interface Payment permits CashPayment, Online {
}
Only the permitted classes and interfaces can directly implement or extend the interface.
public non-sealed interface Online extends Payment {
}

public final class CashPayment implements Payment {
}
Attempting to create another class or interface that directly implements or extends Payment without being listed in the permits clause results in a compilation error.

Just like sealed classes, every direct implementation or extension of a sealed interface must explicitly be declared as final, sealed, or non-sealed.

However, there is one important difference. A class implementing a sealed interface may be declared as final, sealed, or non-sealed, whereas an interface extending a sealed interface can only be declared as sealed or non-sealed, since Java interfaces cannot be final.

Pattern Matching with Sealed Classes

One of the biggest advantages of sealed classes is that the compiler knows every possible subtype in the hierarchy.

This enables safer and more expressive pattern matching, allowing the compiler to verify that all possible cases have been handled.

Without sealed classes, the compiler cannot determine every possible subclass, so pattern matching must usually include a default branch.

Consider the following sealed hierarchy.
public sealed interface Shape permits Circle, Rectangle, Triangle {
}

public record Circle(double radius) implements Shape {
}

public record Rectangle(double length, double width) implements Shape {
}

public record Triangle(double base, double height) implements Shape {
}
Pattern matching can now determine the exact subtype.
public static void printShape(Shape shape) {

    if (shape instanceof Circle circle) {
        System.out.println("Circle : " + circle.radius());
    } else if (shape instanceof Rectangle rectangle) {
        System.out.println("Rectangle : " + rectangle.length());
    } else if (shape instanceof Triangle triangle) {
        System.out.println("Triangle : " + triangle.base());
    }
}
Unlike traditional instanceof, pattern matching automatically performs the type cast.

Exhaustive switch Expressions

Sealed classes become even more powerful when used with Java's modern switch expressions.

Since the compiler knows every permitted subtype, it can verify that every possible case has been handled. In many situations, a default branch is no longer necessary.

Using the previous Shape hierarchy,
public static double area(Shape shape) {
    return switch (shape) {
        case Circle circle ->
                Math.PI * circle.radius() * circle.radius();
        case Rectangle rectangle ->
                rectangle.length() * rectangle.width();
        case Triangle triangle ->
                triangle.base() * triangle.height() / 2;
    };
}
Notice that no default branch is required because every permitted subtype has already been covered. If a new permitted subtype is later added,
public final class Pentagon implements Shape {
}
the compiler immediately reports every switch expression that no longer handles all possible cases. This compile-time verification makes applications easier to maintain as type hierarchies evolve.

Final Notes

Sealed classes introduce controlled inheritance to the Java language, allowing developers to explicitly define which types may extend or implement a class or interface. This produces clearer object models, stronger compile-time guarantees, and safer application designs.

When combined with records, pattern matching, and modern switch expressions, sealed classes enable concise and type-safe representations of domain models, workflow states, commands, events, and abstract syntax trees.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion