Back to Blog
Java

Java Non Sealed Class: Breaking a Sealed Hierarchy

Understand the java non sealed class modifier: its syntax, how it differs from sealed and final, and when to open a sealed hierarchy for extension.

sealed classesJava 17class modifierstype hierarchypattern matching
Illustration of a sealed class hierarchy where one branch is marked non-sealed and opens to unknown subclasses.

In a sealed class hierarchy, the non-sealed modifier is the explicit way to reopen a branch of the hierarchy to unknown subclasses. Understanding when to use a java non sealed class is essential for designing hierarchies that are both closed at the root and open at specific extension points.

What a Non-Sealed Class Actually Does

When a sealed class declares its permitted subclasses, each permitted subclass must choose exactly one of three modifiers: final, sealed, or non-sealed. A non-sealed subclass declares that its own subclasses are not restricted by the original sealed contract. The hierarchy remains sealed at the top level, but that particular branch becomes open again.

public sealed class Shape permits Circle, Square, Triangle { public abstract double area(); } public non-sealed class Triangle extends Shape { private final double base; private final double height; public Triangle(double base, double height) { this.base = base; this.height = height; } @Override public double area() { return 0.5 * base * height; } }

Here Shape is sealed and permits only three direct subclasses. Triangle is declared non-sealed, which means any class can extend Triangle without being listed in a permits clause. The compiler no longer enforces a closed set of subclasses below Triangle.

Sealed, Final, and Non-Sealed: How the Modifiers Differ

Every direct subclass of a sealed class must be marked with exactly one of these three modifiers. The choice determines how far the sealing propagates.

ModifierSubclasses allowedSealing contract
finalNoneBranch ends here
sealedOnly listed subclassesContract continues
non-sealedAny classContract stops at this branch

A final subclass terminates the hierarchy completely. No class can extend it. A sealed subclass continues the contract: it must declare its own permits clause, and its own subclasses must again choose one of the three modifiers. A non-sealed subclass removes the restriction for everything below it.

The key distinction is that non-sealed does not mean "not sealed." It means the class explicitly opts out of the sealed contract for its own subtree. The modifier is required; you cannot simply omit it. A permitted subclass that is neither final, sealed, nor non-sealed is a compile error.

Declaring a Non-Sealed Subclass

The non-sealed modifier appears in the class declaration, before the class keyword. It can be applied to classes and interfaces. Records cannot be non-sealed because records are implicitly final.

public sealed interface Vehicle permits Car, Truck { int wheels(); } public non-sealed interface Truck extends Vehicle { int cargoCapacity(); }

An interface that is non-sealed allows any interface or class to implement it without restriction. This is useful when you want to seal the top level of an API but leave one extension point open for third-party implementations.

The modifier cannot be combined with final. A class cannot be both non-sealed and final; the two are mutually exclusive, and the compiler rejects the combination.

When Non-Sealed Makes Sense

The most common reason to use non-sealed is to preserve an extension point that existed before sealing was introduced. If you migrate an existing class hierarchy to sealed classes, some branches may have been designed for external extension. Marking those branches non-sealed keeps the migration honest: the top level gains the benefits of a closed set, while specific branches retain their original openness.

Another common case is a base class that is itself abstract and intended to be subclassed by framework users. For example, a sealed PaymentMethod with a non-sealed CardPayment branch allows the framework to enumerate known payment types while still letting applications define custom card-based methods.

public sealed abstract class PaymentMethod permits CardPayment, BankTransfer { public abstract void charge(BigDecimal amount); } public non-sealed abstract class CardPayment extends PaymentMethod { public abstract String lastFourDigits(); }

The sealed root gives the compiler and pattern matching a bounded set of direct types. The non-sealed branch gives application code the freedom to add new card payment variants without modifying the library.

Runtime Behavior: Reflection and Pattern Matching

The non-sealed modifier has no runtime representation. It is a compile-time construct. At runtime, a non-sealed class is an ordinary class: Class.isSealed() returns false for it, and getPermittedSubclasses() returns null.

Pattern matching in a switch expression over a sealed type still works when a branch is non-sealed. The compiler knows the direct subclasses of the sealed root, so exhaustiveness checking is performed at compile time. For a non-sealed branch, the pattern that matches the non-sealed type itself is sufficient to cover all of its unknown subclasses.

public String describe(Shape shape) { return switch (shape) { case Circle c -> "circle with radius " + c.radius(); case Square s -> "square with side " + s.side(); case Triangle t -> "triangle with area " + t.area(); }; }

Because Triangle is non-sealed, the Triangle pattern covers every possible subclass of Triangle. The switch remains exhaustive without listing each subclass individually. This is the practical benefit: the closed set at the root still enables exhaustive pattern matching, while the non-sealed branch absorbs an open set of subtypes.

Maintainability and Evolution of the Hierarchy

The decision to mark a branch non-sealed is a long-term commitment. Once a branch is non-sealed, you lose the ability to enumerate all subclasses at compile time. Any code that relies on knowing every concrete type must now handle unknown subtypes.

This affects serialization, equality, and any logic that branches on concrete types. If you add a new sealed branch later, the compiler forces you to update every switch. With a non-sealed branch, the compiler cannot help you find all the places that need to handle new subtypes.

A practical rule: seal what you own, and make non-sealed only what you intend to leave open. If you control all subclasses and do not expect external extension, final or sealed is usually the better choice. If the branch exists specifically to be extended by code outside your module, non-sealed is the correct modifier.

Compatibility Considerations

Sealed classes were introduced as a preview in Java 15 and became final in Java 17. The non-sealed modifier follows the same version requirements. Code that uses it requires a compiler and runtime that support sealed classes, which means Java 17 or later.

When migrating an existing hierarchy, adding sealed to a base class is a source-incompatible change. Every existing direct subclass must be updated to declare final, sealed, or non-sealed. The non-sealed modifier is the least disruptive option for subclasses that were already designed for extension, because it preserves their existing behavior without forcing further changes down the hierarchy.

Binary compatibility is a separate concern. Adding the sealed modifier to an existing class changes its class file attributes. Tools that inspect PermittedSubclasses or isSealed() will observe the change. For most applications this is irrelevant, but for libraries that are serialized or inspected reflectively, the change should be evaluated before release.

java non sealed class: Practical Usage and Code Examples | RYUSLOG DEV