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.
On this page
🔒 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,finalandnon-sealedwork together, and why every subclass must choose - Where permitted subclasses may live (same module or package) and when
permitscan be inferred - How a sealed interface of records models payment states, shapes and expression trees
- Why an exhaustive
switchneeds nodefault, and why adding one throws the benefit away - How the JVM enforces the list with the PermittedSubclasses attribute, and what
MatchExceptionmeans - How to inspect a hierarchy with
Class.isSealed()andgetPermittedSubclasses()
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)
- Pattern matching syntax in depth (guards, record patterns, dominance): covered in Pattern matching for switch
- Records themselves: covered in Records in Java
- Enums, the other closed-set tool: covered in A guide to Java enums
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.
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,sealedornon-sealedis 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-sealedis a real keyword, the first hyphenated one in Java. It exists so reopening is explicit and visible in code review.permitsmust 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.
Shapeis abstract here, but a sealed class can be concrete, in which casenew 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
permitsand 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
Further reading (8)
- JEP 409: Sealed Classes
- JLS §8.1.1.2 sealed, non-sealed, and final Classes
- JLS §9.1.1.4 sealed and non-sealed Interfaces
- JVMS §4.7.31 The PermittedSubclasses Attribute
- JEP 441: Pattern Matching for switch
- Java Language Guide: Sealed Classes and Interfaces
- Javadoc: Class.getPermittedSubclasses()
- Baeldung: Sealed Classes and Interfaces in Java