CodeGym /Courses /JAVA 25 SELF /Sealed classes: syntax and usage

Sealed classes: syntax and usage

JAVA 25 SELF
Level 65 , Lesson 0
Available

1. Sealed class syntax: what it looks like

Let’s start with a classic OOP problem in Java: an open inheritance hierarchy. In regular Java, anyone can extend your class unless it is declared final. That’s convenient, but sometimes leads to surprises — for example, you can’t know in advance which classes will be subclasses of your class, which means you can’t guarantee that you’ve handled all cases in switch or if-else.

As a result, when you write type-based handling (for example, via pattern matching in a switch), you’re forced to add a default branch “just in case”: what if someone somewhere creates a new subclass?

Sealed classes solve this problem: they let you explicitly restrict the list of subclasses. This makes the hierarchy closed and controlled, and your code more predictable and safe.

Basic syntax

Sealed classes appeared in Java 17. They are declared with the sealed modifier, and the list of permitted subclasses is specified with the permits keyword:

public sealed class Shape permits Circle, Rectangle, Square {
    // Common behavior for all shapes
}

Here we declared the Shape class, and only the Circle, Rectangle, and Square classes can be its direct subclasses. No one else can extend Shape — the compiler won’t allow it.

Important: All classes listed in permits must be declared in the same file or be visible to the compiler (usually in the same package). By the way, if all subclasses are declared in the same file as the sealed class, you can omit permits entirely — the compiler will figure it out.

Example:

// Everything in one file - permits is optional
public sealed class Shape {
}
final class Circle extends Shape {}
final class Rectangle extends Shape {}

Requirements for subclasses

Each subclass must explicitly define its status:

  • be final (forbids further inheritance),
  • or be sealed (and continue restricting inheritance),
  • or be non-sealed (allows inheritance, removes restrictions).

Example:

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

public final class Circle extends Shape {}
public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}
public non-sealed class Square extends Shape {}
  • Circle is final; it cannot be subclassed further.
  • Rectangle is itself sealed and permits only two subclasses.
  • Square is non-sealed; anyone can extend it.

Minimal example

public sealed class Animal permits Dog, Cat {}

public final class Dog extends Animal {}
public final class Cat extends Animal {}

Try to declare a new class public class Wolf extends Animal {} — you’ll get a compilation error:

class Wolf is not allowed to extend sealed class Animal

2. Using sealed classes: where and why

Pattern matching and switch

Sealed classes work especially well with pattern matching in a switch. If the compiler knows all possible subclasses, it can verify that you’ve handled each case and won’t even require a default branch.

public sealed interface Result permits Success, Error {}

public final class Success implements Result {
    public final String data;
    public Success(String data) { this.data = data; }
}

public final class Error implements Result {
    public final String message;
    public Error(String message) { this.message = message; }
}

public class Main {
    public static void main(String[] args) {
        Result result = new Success("Hooray!");
        switch (result) {
            case Success s -> System.out.println("Success: " + s.data);
            case Error e -> System.out.println("Error: " + e.message);
        }
    }
}

The compiler knows that there can’t be other variants of Result and doesn’t require default. If you forget to handle one of the cases, the compiler will remind you immediately.

Note: starting with Java 21, if not all cases are handled in a switch, you’ll get a compilation error. In earlier versions (17–20) a default branch may be required, but the IDE will still warn about incomplete coverage.

Safety and control

Sealed classes let the developer fully control the hierarchy. This is especially important for domain models where the set of variants must be fixed (for example, an order state: New, Paid, Cancelled).

Simplifying maintenance and evolution of code

When you know all subclasses, it’s easier to add new features, maintain the code, and refactor. The IDE will also be “aware” of all variants and can help with autocompletion and analysis.

3. Practical examples of using sealed classes

Example 1: Geometric shapes

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

public final class Circle implements Shape {
    public final double radius;
    public Circle(double radius) { this.radius = radius; }
}

public final class Rectangle implements Shape {
    public final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

public final class Square implements Shape {
    public final double side;
    public Square(double side) { this.side = side; }
}

Now you can safely use a switch with pattern matching:

Shape shape = new Circle(5);
switch (shape) {
    case Circle c -> System.out.println("Circle with radius " + c.radius);
    case Rectangle r -> System.out.println("Rectangle " + r.width + "x" + r.height);
    case Square s -> System.out.println("Square with side " + s.side);
}

Example 2: Financial transactions

public sealed interface Transaction permits Deposit, Withdraw, Transfer {}

public final class Deposit implements Transaction {
    public final double amount;
    public Deposit(double amount) { this.amount = amount; }
}

public final class Withdraw implements Transaction {
    public final double amount;
    public Withdraw(double amount) { this.amount = amount; }
}

public final class Transfer implements Transaction {
    public final double amount;
    public final String toAccount;
    public Transfer(double amount, String toAccount) {
        this.amount = amount;
        this.toAccount = toAccount;
    }
}

Now the handler can be sure you didn’t miss any transaction type:

Transaction tx = new Transfer(100, "ACC123");
switch (tx) {
    case Deposit d -> System.out.println("Deposit: " + d.amount);
    case Withdraw w -> System.out.println("Withdrawal: " + w.amount);
    case Transfer t -> System.out.println("Transfer: " + t.amount + " to " + t.toAccount);
}

Example 3: A constrained hierarchy with non-sealed

public sealed class Notification permits EmailNotification, SmsNotification, PushNotification {}

public final class EmailNotification extends Notification {}
public non-sealed class SmsNotification extends Notification {}
public final class PushNotification extends Notification {}

// Now anyone can extend SmsNotification
public class ViberNotification extends SmsNotification {}

4. Features, limitations, and nuances of sealed classes

Modifier requirements

  • A sealed class must explicitly list all subclasses via permits.
  • All subclasses must be final, sealed, or non-sealed.
  • Subclasses must be declared either in the same file or be visible to the compiler.

Abstract sealed classes

A sealed class can be abstract, an interface, or a regular class. For example:

public sealed abstract class Expr permits Const, Add, Mul {}

Compatibility with other modifiers

  • You cannot declare a sealed class as final or non-sealed.
  • An interface can also be sealed (and that’s very handy!).

Using with record classes

Record classes can be subclasses of a sealed class provided they are declared final (by default a record is always final):

public sealed interface Expr permits Const, Add, Mul {}

public record Const(int value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Mul(Expr left, Expr right) implements Expr {}

5. Useful details

Sealed classes and pattern matching: how they relate

The main feature of sealed classes is exhaustive pattern matching. It sounds scary, but it works simply. The compiler knows all possible variants, and you can safely write a switch without a default branch:

Expr expr = ...;
switch (expr) {
    case Const c -> ...
    case Add a -> ...
    case Mul m -> ...
}

If you don’t handle some variant, the compiler won’t let the project build — this is very convenient and safe.

Real-world applications

Where are sealed classes really useful? Anywhere you have a fixed set of variants:

  • Operation result: Success, Error (as in the example above).
  • Order state: New, Paid, Cancelled.
  • Abstract syntax trees (AST) for parsers.
  • API responses: Ok, NotFound, Error.
  • System events: UserLoggedIn, UserLoggedOut, UserRegistered.

6. Common mistakes when working with sealed classes

Mistake #1: forgot to list all subclasses in permits.
If you don’t list all required subclasses via permits, the compiler will complain immediately. For example, if you wrote permits Circle, Rectangle but forgot Square, and such a class exists, you’ll get an error.

Mistake #2: a subclass is not final, sealed or non-sealed.
If a subclass isn’t declared with the required modifier, the compiler will report an error: “Class must be either final, sealed or non-sealed”.

Mistake #3: a subclass is declared in another file and not visible to the compiler.
All classes listed in permits must be available to the compiler — either in the same file or in the same package.

Mistake #4: didn’t handle all cases in switch.
If you use a sealed class in a switch with pattern matching and forget to handle some case, the compiler won’t let the project build. That’s good — you won’t miss any case.

Mistake #5: trying to extend a sealed class without being listed in permits.
If you try to create a subclass that isn’t listed in permits, you’ll get an error: “is not allowed to extend sealed class”.

Mistake #6: trying to use sealed classes on older JDK versions.
Sealed classes appeared only in Java 17. If you try to use them in an older version, you’ll get a compilation error or won’t be able to build the project at all.

1
Task
JAVA 25 SELF, level 65, lesson 0
Locked
Violation of restrictions in the transport system 🚫
Violation of restrictions in the transport system 🚫
1
Task
JAVA 25 SELF, level 65, lesson 0
Locked
Flexible Notification System 📱
Flexible Notification System 📱
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION