Sealed Classes in Java

Mohit jainMohit jain
4 min read

Sealed classes are one of the biggest “modern Java” features that came after Java 8, finalized in Java 17 (LTS). They give you more control over inheritance and make class hierarchies safer and easier to reason about.


1. The problem they solve

In Java 8, if you wrote an abstract class or interface, any class in the world could extend/implement it (unless you made the constructor/package-private). That meant:

  • You couldn’t always predict or restrict the hierarchy.

  • Exhaustive switch or instanceof checks weren’t possible (because subclasses could be added anywhere).

  • This made reasoning, pattern matching, and security harder.


2. What sealed classes do

With a sealed class (or interface), you explicitly list which classes are allowed to extend/implement it.

Syntax:

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

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

Keywords:

  • sealed → restricts which classes can extend it (must be in same package or module).

  • permits → lists the allowed subclasses.

  • final → subclass can’t be extended further.

  • sealed → subclass itself continues the sealing, listing its own permitted subclasses.

  • non-sealed → subclass lifts the restriction, allowing free extension again.


3. Non-Sealed Classes in Java

When you define a sealed class or interface (Java 17+), you explicitly list which classes are allowed to extend/implement it.
But not every subclass needs to remain sealed or final.

That’s where non-sealed comes in:

  • It removes the restriction and allows any other class to extend it.

  • Useful when you want to control the top-level hierarchy but leave some branches open for extensibility.

Example:-

Sealed interface

sealed interface Shape permits Circle, Rectangle, Polygon { }

Sealed and final implementations

final class Circle implements Shape { }
final class Rectangle implements Shape { }

Non-sealed implementation

non-sealed class Polygon extends Shape { }

class Triangle extends Polygon { }
class Hexagon extends Polygon { }

Here:

  • Shape is sealed → only Circle, Rectangle, and Polygon are allowed to implement it.

  • Circle and Rectangle are final → no further subclassing.

  • Polygon is non-sealed → other classes are free to extend it, even outside the original permits list.


4. Do subclasses of a sealed class need final / sealed / non-sealed?

Yes, it’s mandatory.
Every direct subclass of a sealed class (or sealed interface) must explicitly declare one of the following modifiers:

  1. final → no further subclassing allowed.

  2. sealed → subclass is also sealed, and must declare its own permits list.

  3. non-sealed → subclass removes restrictions, allowing free subclassing again.

Why this rule?

  • Ensures clarity of hierarchy → the compiler knows if the subclass hierarchy is closed, open, or final.

  • Makes exhaustive pattern matching possible in switch.

  • Prevents ambiguity (you always know whether further extension is possible).

If you omit final, sealed, or non-sealed in a permitted subclass, the compiler throws an error.


5. Why it matters

  • Safer modeling: You can encode business rules in the type system. Example: A PaymentMethod sealed to only Card, UPI, Wallet.

  • Exhaustive checks: With pattern matching for switch (Java 21), the compiler can verify you’ve covered all cases of a sealed hierarchy.

  • Better readability & maintainability: Readers instantly know the closed set of implementations.


6. Example with pattern matching

sealed interface PaymentMethod permits Card, UPI, Wallet { }

record Card(String number) implements PaymentMethod { }
record UPI(String id) implements PaymentMethod { }
record Wallet(String provider) implements PaymentMethod { }

public String process(PaymentMethod p) {
    return switch (p) {
        case Card c   -> "Processing card " + c.number();
        case UPI u    -> "Processing UPI " + u.id();
        case Wallet w -> "Processing wallet " + w.provider();
    }; // compiler ensures this switch is exhaustive
}

7. Where you’ll use them

  • Modeling fixed domain hierarchies (payment types, shapes, states in a workflow, commands/events).

  • Working with pattern matching (switch + records).

  • Replacing “enums with data”: instead of an enum plus multiple unrelated data holders, you can model each as a sealed subtype.

1
Subscribe to my newsletter

Read articles from Mohit jain directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mohit jain
Mohit jain

Oracle‬‭ Certified‬‭ Java‬‭ Developer‬‭ with‬‭ 7+ years‬‭ of‬‭ experience‬‭ specializing‬‭ in‬‭ backend‬‭ development,‬‭ microservices‬ architecture,‬‭ and‬‭ cloud-based‬‭ solutions.‬‭ Proven‬‭ expertise‬‭ in‬‭ designing‬‭ scalable‬‭ systems,‬‭ optimizing‬‭ performance,‬‭ and‬ mentoring‬‭ teams‬‭ to‬‭ enhance‬‭ productivity.‬‭ Passionate‬‭ about‬‭ building‬‭ high-performance‬‭ applications‬‭ using‬‭ Java,‬‭ Spring‬ Boot, Kafka, and cloud technologies (AWS/GCP)