Course contents

Sealed classes and interfaces

Sealed classes and interfaces from Java 17: sealed, permits, final and non-sealed, module and package rules, inferred permits, records as subtypes, exhaustive switch, domain modelling, PermittedSubclasses and reflection.

PremiumGood to knowLesson14 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a guest list at the door
  2. Sealed types under the hood
  3. Real world: modelling domains that really are closed
  4. Advanced: how the JVM enforces the list
  5. How to say it in the interview

🔒 What are sealed classes and interfaces?

A sealed class or interface lists exactly which types may extend it, and the compiler and the JVM both refuse anyone else. You declare it with sealed ... permits A, B, C, and each permitted subclass must say whether it is final, sealed again, or non-sealed. For example, sealed interface PaymentState permits Pending, Authorized, Captured, Failed means a payment can be in exactly those four states, and a switch over it can cover all four without a default.

Sealed types arrived as a final feature in Java 17 (JEP 409), and with pattern matching for switch finalised in Java 21 they've become the standard way to model "one of a fixed set of shapes" in Java. Interviewers ask about them for two reasons. First, they test whether you're current: plenty of candidates still answer "use an enum or make the constructor package-private". Second, they open a design conversation: how would you model order states, API responses or an expression tree so the compiler catches the case you forgot?

What you’ll learn

  • How sealed, permits, final and non-sealed work together, and why every subclass must choose
  • Where permitted subclasses may live (same module or package) and when permits can be inferred
  • How a sealed interface of records models payment states, shapes and expression trees
  • Why an exhaustive switch needs no default, and why adding one throws the benefit away
  • How the JVM enforces the list with the PermittedSubclasses attribute, and what MatchException means
  • How to inspect a hierarchy with Class.isSealed() and getPermittedSubclasses()

For this lesson, we'll focus on the sealed, permits, final and non-sealed rules, where subclasses are allowed to live, when permits can be left out, records as permitted subclasses, exhaustive switch, domain modelling with sealed hierarchies, and how the class file and reflection expose all of it.

Below the line (out of scope)

The 30-second answer

A sealed class or interface restricts its direct subtypes to a list in its permits clause, finalised in Java 17 by JEP 409. Each permitted subclass must be final, sealed (restricting further) or non-sealed (reopening the hierarchy). Permitted subclasses must be in the same module, or in the same package if you're on the classpath, and permits can be omitted when they're all in the same source file. Records work perfectly as permitted subclasses because they're implicitly final. The payoff is exhaustiveness: a switch over a sealed type with a case for each subtype needs no default, and adding a new subtype turns every incomplete switch into a compile error. The list is stored in the PermittedSubclasses class-file attribute, enforced by the JVM at class load, and visible through Class.isSealed() and getPermittedSubclasses().

Interview radar

Asked very often: Expect questions about sealed, permits, final, sealed again, non-sealed, records as permitted subclasses, and why exhaustive switches need no default. Senior rounds probe: Senior rounds probe same-module or same-package placement, inferred permits, PermittedSubclasses, stale switches throwing MatchException, and why non-sealed reopens only one branch. Production angle: In production this is about modelling fixed business states so adding a new state breaks incomplete handlers during compilation instead of after release.

01Picture it first: a guest list at the door

Think of a private dinner with a printed guest list. The host (the sealed type) names every guest. Each guest decides one more thing: come alone (final), bring a specific named plus-one (sealed with its own list), or say "anyone I invite is welcome" (non-sealed, which reopens the door below that guest). Nobody else gets in, and because the host knows the full list, the host can plan seating for every single guest without a "miscellaneous" table.

The diagram below shows one hierarchy using all three choices. Circle is closed, Rectangle allows only Square, and Polygon deliberately opens up so anyone can add Hexagon.

Fig. 1

A sealed abstract Shape permits Circle, Rectangle and Polygon; Circle is final, Rectangle is sealed and permits only Square, and Polygon is non-sealed so any class such as Hexagon may extend it

A sealed abstract Shape permits Circle, Rectangle and Polygon; Circle is final, Rectangle is sealed and permits only Square, and Polygon is non-sealed so any class such as Hexagon may extend it

Notice that the guarantee is about direct subtypes. Shape knows its three children exactly, and a switch over Shape can rely on that, even though Polygon has opened its own branch. A case Polygon p still covers Hexagon, because a Hexagon is a Polygon.

02Sealed types under the hood

Try it yourself first

Before reading on, write the Shape hierarchy from the diagram. Then predict which of these fails to compile: a Triangle extends Shape, a class Rectangle extends Shape with no extra modifier, and class Hexagon extends Polygon.

public sealed abstract class Shape permits Circle, Rectangle, Polygon { }

public final class Circle extends Shape {                 // closed: no subclasses
    private final double radius;
    public Circle(double radius) { this.radius = radius; }
}

public sealed class Rectangle extends Shape permits Square { }   // restricted again
public final class Square extends Rectangle { }

public non-sealed class Polygon extends Shape { }          // reopened on purpose
public class Hexagon extends Polygon { }                   // ✅ anyone may extend Polygon

// class Triangle extends Shape { }
// ❌ class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)

// class Rectangle extends Shape { }   (without sealed/final/non-sealed)
// ❌ sealed, non-sealed or final modifiers expected

Notice four things:

  • Every permitted subclass must choose. final, sealed or non-sealed is mandatory on each direct subclass of a sealed class. The compiler forces the author to decide what happens below, instead of leaving it open by accident.
  • non-sealed is a real keyword, the first hyphenated one in Java. It exists so reopening is explicit and visible in code review.
  • permits must match reality both ways. Every listed class must actually extend the sealed type, and every direct subclass must be listed. A sealed type with no subclasses at all is an error too (sealed class must have subclasses).
  • Sealed and abstract are independent. Shape is abstract here, but a sealed class can be concrete, in which case new Shape() is allowed as one more "case".

Where subclasses may live

The permitted subclasses must be close to the sealed type, because the compiler needs to see them all when it compiles the sealed type:

  • In a named module (you have a module-info.java), they must be in the same module, and may be in different packages.
  • In the unnamed module (plain classpath), they must be in the same package.
  • If all subclasses are declared in the same source file as the sealed type, you can omit permits and the compiler infers the list. This is the usual style for small hierarchies, often with the subclasses nested inside.

Sealed interfaces work the same way, with permits listing classes or interfaces that implement or extend it. Two kinds of class can never be permitted: anonymous classes (anonymous classes must not extend sealed classes) and lambdas. A sealed interface with one abstract method is not a functional interface, because a lambda would create an unnamed class the permits list can't name, so PaymentState s = () -> ... reports S is not a functional interface. Local classes can't be permitted subclasses either.

Records and sealed interfaces: algebraic data types

Try it yourself first

Model a payment as exactly one of: pending, authorised with an amount and auth code, captured with an amount, or failed with a reason. Then write a method that turns any state into a status message, without a default branch.

public sealed interface PaymentState permits Pending, Authorized, Captured, Failed { }

public record Pending(String orderId) implements PaymentState { }
public record Authorized(String orderId, BigDecimal amount, String authCode) implements PaymentState { }
public record Captured(String orderId, BigDecimal amount) implements PaymentState { }
public record Failed(String orderId, String reason) implements PaymentState { }

static String describe(PaymentState state) {
    return switch (state) {                                   // no default: the compiler knows all four
        case Pending p -> "waiting for " + p.orderId();
        case Authorized(var id, var amount, var code) -> "authorised " + amount + " (" + code + ")";
        case Captured c when c.amount().signum() == 0 -> "captured nothing";
        case Captured c -> "captured " + c.amount();
        case Failed f -> "failed: " + f.reason();
    };
}

System.out.println(describe(new Authorized("o-1", new BigDecimal("499.00"), "A7")));
// prints: authorised 499.00 (A7)
System.out.println(describe(new Failed("o-2", "card declined")));
// prints: failed: card declined

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Real world: modelling domains that really are closed
  • Advanced: how the JVM enforces the list
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicAbstract classes & interfaces: interview questions & scenarios
Further reading (8)