Back to Blog
Java

Java Sealed Keyword: Restricting Inheritance

Learn how the java sealed keyword restricts class hierarchies, enables exhaustive pattern matching, and improves maintainability in Java 17+.

JavaSealed ClassesInheritancePattern MatchingJava 17Type Safety
Diagram showing a sealed class with a limited set of permitted subclasses, illustrating restricted inheritance in Java.

The java sealed keyword controls which classes or interfaces can extend or implement a given type. Introduced as a final feature in Java 17, sealed classes give you a way to declare that a hierarchy is closed to further extension beyond a fixed set of permitted subtypes. This is not just a syntax nicety; it changes how the compiler can reason about exhaustiveness in pattern matching and switch expressions.

What the Sealed Keyword Restricts

In an unsealed class hierarchy, any class in the same package or module can extend a public class, and any interface can be implemented by any class. That openness is useful for frameworks and libraries, but it becomes a problem when you want to model a closed domain, such as a set of payment methods, AST nodes, or geometric shapes. Without a way to limit subtypes, the compiler cannot guarantee that a switch over all possible types is complete.

The sealed keyword addresses this by requiring you to declare the complete set of permitted subclasses at the point of declaration. The compiler then enforces that no other class can extend or implement the sealed type. This restriction is checked at compile time, so a violation results in a compilation error rather than a runtime surprise.

Declaring a Sealed Class or Interface

To declare a sealed class, you use the sealed modifier and a permits clause that lists the allowed subtypes. Each permitted subtype must be in the same module or, if in the unnamed module, the same package. Here is a minimal example:

public sealed class Shape permits Circle, Rectangle, Triangle { }

Each permitted subclass must then declare itself as final, sealed, or non-sealed. A final subclass ends the hierarchy. A sealed subclass continues the restriction with its own permitted subtypes. A non-sealed subclass opens the hierarchy again, allowing any further extension. This gives you fine-grained control over how far the restriction propagates.

public final class Circle extends Shape { } public sealed class Rectangle extends Shape permits Square { } public non-sealed class Triangle extends Shape { }

Sealed interfaces work the same way. A sealed interface lists its permitted implementations, and each implementation must be final, sealed, or non-sealed.

public sealed interface Operation permits Add, Subtract, Multiply { int apply(int a, int b); } public final class Add implements Operation { public int apply(int a, int b) { return a + b; } } // ... other implementations

Rules for Permitted Subclasses

The permits clause is not optional. If you declare a class as sealed, you must list every direct subclass. The compiler verifies that the list is complete and that each listed class actually extends the sealed class. A few rules govern this relationship:

  • The permitted subclasses must be directly adjacent in the inheritance hierarchy. You cannot list a class that is not a direct child.
  • The permitted subclasses must be accessible at the point of declaration. In practice, they must reside in the same module or package.
  • Each permitted subclass must explicitly declare its own sealing status (final, sealed, or non-sealed). No default is assumed.
  • If a permitted subclass is sealed, it must also have its own permits clause, and the chain continues.

These rules ensure that the compiler can compute the complete set of leaf types at compile time. That set is what enables exhaustive pattern matching.

Why Seal a Hierarchy: Exhaustive Pattern Matching

The most immediate benefit of sealed classes is the ability to write exhaustive switch expressions and pattern matching without a default case. When you switch over a sealed type, the compiler knows all possible subtypes and can verify that every case is covered. If a new permitted subtype is added later, any switch that does not handle it will fail to compile.

Consider a shape hierarchy with Circle, Rectangle, and Triangle. A method that calculates area can use a switch expression:

public double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height(); }; }

Because Shape is sealed, the compiler knows that Circle, Rectangle, and Triangle are the only possible runtime types. No default branch is needed, and the compiler will warn or error if a case is missing. This moves a whole class of runtime errors to compile time.

Pattern matching with instanceof also benefits. When you use pattern matching on a sealed type, the compiler can narrow the type in each branch without an explicit cast, and it can verify that the pattern set is exhaustive.

Sealed vs. Final vs. Abstract

It is easy to confuse sealed classes with final or abstract classes, but they serve different purposes. A final class cannot be extended at all. An abstract class can be extended but places no limit on the number or identity of subclasses. A sealed class allows a fixed, explicitly enumerated set of subclasses.

ModifierExtension allowed?Subclass list known at compile time?Typical use case
finalNoN/AImmutable value types, utility classes
abstractYes, unlimitedNoBase classes with shared behavior
sealedYes, only listedYesClosed domain models, algebraic data types

Sealed classes are often the right choice when you want the flexibility of an abstract base class but also want the compiler to enforce a closed set of variants. This is common in domain-driven design, where a business concept has a known set of categories.

Common Mistakes and How to Avoid Them

One frequent mistake is forgetting to list a subclass in the permits clause. If you define a class that extends a sealed class but is not listed, the compiler rejects it. The error message is clear, but it can be confusing when you are adding a new subtype. Remember to update the permits clause in the parent.

Another mistake is declaring a permitted subclass as neither final, sealed, nor non-sealed. The compiler will complain that the subclass must specify one of these modifiers. This is a deliberate design choice to force you to think about how far the hierarchy should extend.

A third issue arises when a sealed class is in a different package from its permitted subclasses. In the unnamed module, all permitted subclasses must be in the same package. If you try to split them across packages, the compiler will refuse. This is a real constraint, so plan your package structure accordingly.

Finally, do not use non-sealed casually. It reopens the hierarchy and defeats the purpose of sealing. Use it only when you have a specific reason to allow arbitrary extension from that point downward.

Runtime Behavior and Reflection

Sealedness is a compile-time concept, but it also has a runtime presence. The Class object for a sealed class exposes its permitted subclasses via the getPermittedSubclasses() method. This method returns an array of Class objects representing the direct subclasses that were listed in the permits clause. The array is empty for non-sealed classes.

Class<Shape> shapeClass = Shape.class; Class<?>[] permitted = shapeClass.getPermittedSubclasses();

This reflection API is useful for frameworks that need to discover all subtypes of a sealed hierarchy at runtime, such as serialization libraries or dependency injection containers. However, note that the list is fixed at compile time; you cannot add a new subclass at runtime without recompiling.

Another runtime consideration is that the JVM does not enforce sealing at the bytecode level. A malicious or careless library could bypass the restriction using reflection or bytecode manipulation. Sealed classes are a language-level guarantee, not a security boundary. In normal application code, the compiler prevents accidental violations, but you should not rely on sealing for security.

When to Use Sealed Classes in Production Code

Sealed classes shine in scenarios where the domain has a fixed, known set of variants. Common examples include:

  • AST nodes in a compiler or interpreter
  • JSON value types (object, array, string, number, boolean, null)
  • State machines with a finite set of states
  • Command or event types in an event-sourcing system

In each case, the sealed hierarchy makes it impossible to add an unhandled variant without the compiler noticing. This reduces the risk of IllegalStateException or missing branches in switch statements.

Sealed classes also work well with records. A record is implicitly final, so it can be a permitted subclass without extra effort. Combining sealed interfaces with records gives you a concise way to define algebraic data types in Java.

public sealed interface Result<T> permits Success, Failure { record Success<T>(T value) implements Result<T> {} record Failure<T>(String error) implements Result<T> {} } ```n Here, `Result` can only be a `Success` or a `Failure`, and both are records. This pattern is ideal for representing operations that can succeed or fail without using exceptions. When you design a public API, consider whether sealing is appropriate. If you expect third-party developers to extend your types, sealing will prevent that. If you want to control the hierarchy for safety and maintainability, sealing is the right tool. Many library authors use sealed interfaces to prevent users from implementing the interface themselves, forcing them to use the provided implementations. One operational advantage of sealed classes is that they make code easier to refactor. When you add a new permitted subtype, the compiler immediately points to every switch and pattern match that needs a new case. This is far better than discovering a missing case in production through a runtime error. In summary, the `java sealed keyword` is a powerful tool for expressing closed hierarchies. It gives you compile-time exhaustiveness, clearer domain models, and safer refactoring. Use it when you want to restrict inheritance to a known set of subtypes, and combine it with records and pattern matching for a modern, concise style of Java programming.
java sealed keyword: Practical Usage and Code Examples | RYUSLOG DEV