Topic 5.11
Sealed Classes and Interfaces
In one line
A sealed class or interface lists exactly which classes may extend or implement it, with permits. The family is closed: the compiler and JVM reject any other subclass, and (from Java 21) a switch over a sealed type can be checked for completeness without a default.
Think of it like this
A traffic light. It has exactly three colours: red, amber and green. Nobody can add a purple light. Because the list is closed, a driving instructor can say 'for each colour, here's what you do' and be sure nothing is missing. A sealed type is a closed list of kinds, so code that handles 'each kind' can be checked for completeness.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- sealed
- A modifier for a class or interface that restricts which classes may directly extend or implement it.
- permits
- The clause listing the classes allowed to extend a sealed type:
permits Circle, Square. - non-sealed
- A modifier for a permitted subclass that opens it back up, so any class can extend it.
- Exhaustive
- Covering every possible case. A switch over a sealed type with a case for each permitted subtype is exhaustive.
- Algebraic data type
- A type defined as a fixed set of alternatives, each holding some data. In Java: a sealed interface plus records.
- Record
- A compact, immutable data class (Java 16, Topic 4.9). Records are implicitly final.
- Pattern matching for switch
- A Java 21 feature letting
caselabels test types, likecase Circle c ->, and bind a variable.
Step by step
01The problem: open hierarchies can't be checked
Suppose Shape is an ordinary interface. You write area(Shape s) with instanceof checks for Circle, Square and Rectangle. Next month someone adds Triangle implements Shape. Your method compiles fine and then fails at run time with your 'unknown shape' exception, or worse, silently returns 0.
The compiler couldn't help, because an open interface may have any number of implementations, including ones in code it has never seen.
02Close the family with sealed and permits
Declare the type sealed and list its children with permits. Each child must be final, sealed or non-sealed; records are final automatically.
Now Triangle implements Shape doesn't compile unless you add it to the permits list, which makes the change deliberate and visible in one place.
sealed interface Shape permits Circle, Square, Rectangle { }
record Circle(double r) implements Shape { } // records are final
record Square(double side) implements Shape { }
record Rectangle(double w, double h) implements Shape { }03final, sealed or non-sealed: each child decides
final: the branch stops here. The most common choice.
sealed: the child has its own closed list (sealed class Truck extends Vehicle permits HeavyTruck).
non-sealed: the child is open again; anyone may extend it. Use it when one branch is meant for extension (say, a plugin point) while the rest stays closed. Exhaustiveness still works: a case Bike b covers Bike and all its unknown subclasses.
04What the compiler rejects
A class not listed in permits: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause). (The message says 'class' and 'extend' even for interfaces.)
A permitted class without final, sealed or non-sealed: sealed, non-sealed or final modifiers expected.
A permits entry that doesn't actually extend the sealed type: invalid permits clause.
05Exhaustive switch (Java 21)
With Java 21's pattern matching for switch, you can write return switch (s) { case Circle c -> ...; case Square q -> ...; case Rectangle r -> ...; }; with no default. The compiler checks that the cases cover every permitted subtype.
Remove a case, or add a new permitted subtype, and you get the switch expression does not cover all possible input values. That's the real power of sealed types: adding a variant turns every place that must handle it into a compile error, instead of a production bug.
On Java 17, sealed types already work, but switch patterns were only a preview, so Java 17 code uses instanceof chains with a final throw for the impossible case.
06It's in the class file, and visible by reflection
javap -v Shape shows a PermittedSubclasses attribute listing the children. When the JVM loads a class whose direct superclass or superinterface is sealed, it checks the list and throws IncompatibleClassChangeError if the class isn't on it.
Reflection (Java 17) exposes the same data: Shape.class.isSealed() and Shape.class.getPermittedSubclasses().
Try it yourself
- 1
Add a variant and let the compiler guide you
In the Payment example (Java 21), add
record Wallet(double amount) implements Payment { }and addWallettopermits. Compile: both switches fail with 'does not cover all possible input values'. Add acase Wallet w -> ...to each and run again. - 2
Try to sneak in a subclass
In the shapes example, add
record Triangle(double b, double h) implements Shape { }without changingpermits. Read the error. Then add it topermits, and noticearea()still compiles (instanceof chains aren't checked) but throws at run time for a Triangle. That's exactly why the Java 21 switch is better. - 3
Drop the modifier
In the Vehicle example, remove
finalfromCar. Readsealed, non-sealed or final modifiers expected.
Code & diagrams
Expected output
Circle[r=1.0] -> 3.14
Square[side=2.0] -> 4.00
Rectangle[w=2.0, h=5.0] -> 10.00
Shape is sealed: true
permits Circle
permits Square
permits RectangleExpected output
Car has 4 wheels
Truck has 6 wheels
HeavyTruck has 18 wheels
Bike has 2 wheels
EBike has 2 wheels
Vehicle: sealed
Car: final
Truck: sealed
Bike: openNeeds Java 21. Add a fourth record to the permits list and both switches stop compiling until you handle it.
Expected output
Card ending 4242: 2500.0 | fee 50.0
Card ending 1111: 300.0 | fee 5.0
UPI asha@bank: 800.0 | fee 0.0
Cash: 150.0 | fee 0.0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Implement a sealed interface without permission
Add Triangle that implements Shape but isn't in permits.
sealed interface Shape permits Circle, Square { }
final class Circle implements Shape { }
final class Square implements Shape { }
final class Triangle implements Shape { }Break #2
Forget final, sealed or non-sealed
Declare a permitted subclass as a plain class.
sealed interface Shape permits Circle { }
class Circle implements Shape { }Break #3
Miss a case in a switch (Java 21)
Switch over a sealed Shape with a case for Circle only.
sealed interface Shape permits Circle, Square { }
record Circle(double r) implements Shape { }
record Square(double s) implements Shape { }
public class Main {
static double area(Shape s) { return switch (s) { case Circle c -> 3.0 * c.r(); }; }
public static void main(String[] a) { } }Myth vs fact
Myth
Sealed means the subclasses can't have their own subclasses.
Fact
Each permitted subclass chooses: final stops, sealed continues with its own closed list, non-sealed reopens the branch.
Myth
Sealed is the same as package-private constructors.
Fact
A package-private constructor hides extension from other packages, but the compiler can't use it for exhaustiveness, and the type can't be public-and-implementable-only-by-you as an interface. Sealed is declared, checked by the JVM, and visible to the compiler and reflection.
Myth
You need Java 21 to use sealed classes.
Fact
Sealed classes are standard in Java 17 (previewed in 15 and 16). Java 21 added pattern matching for switch, which makes them far more useful.
Interview problem
The problem
Model an API result
Design a type for the result of calling a payment API. It can succeed with a transaction id, fail with an error code and message, or be pending with a retry-after time in seconds. Callers must handle every outcome, and adding a new outcome later must force callers to update.
You're given
- Java 21
- No nulls for 'missing' data
- Exhaustiveness checked by the compiler
The interviewer follows up
Why not an enum?
Why not throw exceptions for failures?
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Exhaustive switches over sealed types are still protected at run time: if a new subclass appears without recompiling the switch (separate compilation), the compiler-generated default throws
MatchException(Java 21) instead of silently falling through. - ▸
JEP 409 also tightened cast checking: with sealed hierarchies the compiler can prove more casts impossible. For a sealed interface whose implementations are all final classes, a cast from an unrelated final class is rejected at compile time.
- ▸
Sealing is per direct subtype only.
permitslists direct children; it says nothing about classes further down anon-sealedbranch. Exhaustiveness still works because acasefor the non-sealed class covers its whole subtree. - ▸
Design guidance: sealed types model closed sets of *alternatives* (states, messages, AST nodes, results); polymorphism via overridden methods models open sets of *implementations*. Pattern matching makes adding operations easy and variants hard; overriding makes adding variants easy and operations hard (the 'expression problem'). Pick per domain. The State pattern (System Design course,
/topic/phase-2/state) is a classic case where a sealed set of states fits well.
Remember this
- 1
Between
final(no subclasses) and an ordinary class (anyone can subclass), sealed gives a third option: these subclasses and no others. Writesealed interface Shape permits Circle, Square, Rectangle { }. - 2
Every permitted subclass must say how *it* continues the hierarchy, with exactly one of: **
final(no further subclasses),sealed(its own closed list), ornon-sealed** (open again to anyone). Records and enums are implicitly final, so a record can implement a sealed interface with no extra modifier. - 3
Location rules: permitted subclasses must be in the same module as the sealed type, or, if the code isn't in a named module (the usual case for small programs), in the same package. If all subclasses are in the same source file, you may leave out
permitsand the compiler infers the list. - 4
The payoff is exhaustiveness. Because the compiler knows every subtype, a
switchover a sealed type with a case for each subtype needs nodefault(pattern matching for switch, Java 21, Topic 11.2). Add a new subtype later and every such switch stops compiling until it handles the new case. The compiler finds your missing cases for you. - 5
Sealed interfaces plus records give Java algebraic data types: a closed set of alternatives (sum type), each a simple data carrier (product type). This is the modern way to model things like
Payment = Card | Upi | CashorResult = Success | Failure. - 6
It's enforced at run time too. The class file records a
PermittedSubclassesattribute, and the JVM refuses to load a class that extends a sealed type without being listed, so a sealed hierarchy can't be bypassed by compiling a rogue class separately.
Explain it without notes
What do sealed, permits, final and non-sealed each do?
Where must permitted subclasses live?
How do sealed types make switch statements safer?
How is sealing enforced at run time?
Practice
Model sealed interface Expr permits Num, Add, Mul with records, and write a Java 17-compatible eval(Expr) using instanceof patterns. Evaluate (2 + 3) * 4.
Model traffic-light states as a sealed interface with three records, and write next(Light) that cycles Red → Green → Amber → Red. Print four steps from Red.
Write a sealed class Account with final subclasses Savings and Current, then print Account.class.getPermittedSubclasses() names.
Trade-offs
- ↔
Sealing gives compile-time completeness checks and stronger domain models, but closes the hierarchy to outside extension. Library authors must be sure no user needs their own subtype.
- ↔
Adding a permitted subtype is a source-breaking change for every exhaustive switch in client code. That's the point inside one codebase, but a compatibility burden across published library versions.
Done when you can
Done when you can declare a sealed interface with records as permitted subtypes.
Done when you can explain
final,sealedandnon-sealedon the children.Done when you can write an exhaustive Java 21 switch over a sealed type with no default.
Done when you can explain the package/module rule and run-time enforcement.
Done when you can choose between a sealed hierarchy and open polymorphism for a design.