Skip to content

Sealed Classes — Permits, Sealed Interfaces, and Exhaustive Switches (Java 17+)

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Sealed Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

Sealed classes in Java restrict which other classes can extend or implement them, enabling exhaustive pattern matching. Before sealed classes, any class could be extended by any subclass, making it impossible to reason about all possible subtypes of a given type.

What You'll Learn

  • Declaring sealed classes with the permits clause
  • Sealed interfaces and records
  • Exhaustive pattern matching with sealed types
  • Migration from unrestricted to sealed hierarchies

Why It Matters

Sealed classes make your code's intent explicit. When a class is sealed, anyone reading the code knows exactly which subclasses exist. This enables the compiler to verify exhaustiveness in switch expressions — if you handle all permitted subtypes, you do not need a default branch.

Real-World Use

Algebraic data types in Functional Programming, state machines (each state is a sealed subtype), AST nodes in compilers, and JSON token types in parsers.


Basic Sealed Class

public sealed class Shape permits Circle, Rectangle, Triangle {
    // common fields or methods
}

final class Circle extends Shape {
    double radius;
}

final class Rectangle extends Shape {
    double width, height;
}

final class Triangle extends Shape {
    double base, height;
}

Rules:

  • The permits clause lists all allowed subclasses
  • Each permitted subclass must be final, sealed, or non-sealed
  • Permitted classes must be in the same module (or the same package for unnamed modules)

Sealed Interface

public sealed interface JsonValue permits JsonString, JsonNumber, JsonObject, JsonArray {
    String toJson();
}

record JsonString(String value) implements JsonValue {
    public String toJson() { return "\"" + value + "\""; }
}

record JsonNumber(double value) implements JsonValue {
    public String toJson() { return String.valueOf(value); }
}

Interfaces benefit from sealing by enabling exhaustive pattern matching over implementations.

The Three Subclass Modifiers

Each permitted subclass must choose one of three modifiers:

final

No further subclassing allowed:

final class Circle extends Shape { }

sealed

The subclass itself is sealed, creating a deeper hierarchy:

sealed class Polygon extends Shape permits Quadrilateral, Pentagon { }

non-sealed

Opens the hierarchy again — any class can extend this subclass:

non-sealed class FreeShape extends Shape { }
// Now any class can extend FreeShape

Exhaustive Switch with Sealed Classes

The real power of sealed classes comes with pattern matching in switch:

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;
        // No default needed — all subtypes covered
    };
}

If you add a new permitted subclass later, the compiler warns you about non-exhaustive switch expressions. This is a compile-time safety guarantee that prevents runtime errors.

Sealed Classes and Records

Records are implicitly final, making them natural permitted subtypes:

public sealed interface Payment permits CreditCard, PayPal, Crypto { }

public record CreditCard(String cardNumber, String expiry) implements Payment { }
public record PayPal(String email) implements Payment { }
public record Crypto(String walletAddress, String currency) implements Payment { }

Common Mistakes

  1. Forgetting to list a subclass in permits. The subclass declaration compiles, but the sealed class does not permit it — the compiler reports an error.
  2. Omitting final, sealed, or non-sealed on a permitted subclass. Every permitted class must explicitly declare one of these three modifiers, or the compiler reports an error.
  3. Adding a default branch in a switch over a sealed type. A default branch defeats the exhaustiveness check. Skip the default if all cases are covered.
  4. Using sealed classes when an enum would suffice. If the variants have no state or behavior differences, an enum is simpler. Use sealed classes when each variant has different fields or methods.
  5. Putting permitted classes in different modules without exporting. In a modular JAR, the permitted classes must be in the same module or be explicitly accessible.

Practice Questions

1. What problem do sealed classes solve?
They make the set of subtypes explicit and finite, enabling exhaustive pattern matching and preventing unauthorized subclassing.

2. What are the three allowed modifiers for a subclass of a sealed class?
final (no further subclasses), sealed (continues the sealing), non-sealed (opens the hierarchy).

3. How does exhaustive pattern matching work with sealed classes?
The compiler knows all permitted subtypes. If a switch expression covers all of them, no default branch is needed. If a new subtype is added, the switch fails to compile until updated.

4. Can a sealed class have subclasses that are records?
Yes. Records are implicitly final, so they are valid permitted subtypes.

5. What happens if I omit the permits clause?
The compiler infers the permitted subclasses from the compilation unit. In Java 17+, as long as all permitted subclasses are in the same file, you can omit permits.

Challenge Question:
Design a sealed hierarchy for a simple expression evaluator:

sealed interface Expr permits Constant, Add, Multiply, Negate { }

record Constant(int value) implements Expr { }
record Add(Expr left, Expr right) implements Expr { }
record Multiply(Expr left, Expr right) implements Expr { }
record Negate(Expr expr) implements Expr { }

Write an evaluate(Expr e) method using pattern matching in switch that computes the result.

FAQ

Can sealed classes be used with reflection?

Yes. The java.lang.Class API includes getPermittedSubclasses() (Java 9+) that returns an array of permitted subtypes. You can inspect sealed hierarchies at runtime.

What is the difference between sealed and final?

final prevents all subclassing. sealed allows controlled subclassing — you specify exactly which classes can extend the sealed class. Sealed is more flexible than final when you need a known, finite set of subtypes.

Can a sealed class be abstract?

Yes. Sealed abstract classes are common — the base class provides common fields/constructors, and the permitted subclasses provide specific implementations.

Do I need to use `permits` if permitted classes are in the same file?

If all permitted classes are in the same compilation unit (same .java file), the permits clause can be omitted — the compiler infers them. This is called implicit sealing.

How do sealed classes relate to the Open/Closed Principle?

Sealed classes support the Open/Closed Principle at the module level. The hierarchy is closed (new subtypes cannot be added externally) but open for extension within the permitted types. This is useful for domain modeling where the set of types is fixed.

Mini Project

Write a program SealedDemo.java that:

  1. Declares a sealed interface Vehicle permitted to Car, Bicycle, Truck
  2. Car is a record with make, model, seats (int)
  3. Bicycle is a record with type (String)
  4. Truck is a record with capacity (double)
  5. Adds a method String describe() to Vehicle — each record implements it
  6. Creates a method void printDescription(Vehicle v) using a switch expression with pattern matching — no default branch
  7. Creates a list of vehicles and prints their descriptions
  8. Shows what happens if you add a new Motorcycle subtype — the switch fails to compile

What's Next

Sealed classes complete the modern Java type system. With classes, interfaces, enums, records, and sealed hierarchies, you have a rich toolkit for modeling data. Starting with Lesson 21, you will learn Java's core APIs — beginning with exception handling and the try/catch/finally mechanism.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro