Command Palette

Search for a command to run...

PHASE 5Intermediate Java 17+ ~31 min· topic 11 of 13

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 case labels test types, like case 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.

Main.javawhole filejava
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.

final, sealed or non-sealed: each child decidesdiagram
Rendering diagram…

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.

terminal
$ javac Main.java
── expected output ──
Main.java:4: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
final class Triangle implements Shape { }
^
1 error

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.

terminal
$ javac --release 21 Main.java
── expected output ──
Main.java:5: error: the switch expression does not cover all possible input values
static double area(Shape s) { return switch (s) { case Circle c -> 3.0 * c.r(); }; }
^
1 error

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().

terminal
$ javap -v Shape
── expected output ──
...
PermittedSubclasses:
Circle
Square

Try it yourself

  1. 1

    Add a variant and let the compiler guide you

    In the Payment example (Java 21), add record Wallet(double amount) implements Payment { } and add Wallet to permits. Compile: both switches fail with 'does not cover all possible input values'. Add a case Wallet w -> ... to each and run again.

  2. 2

    Try to sneak in a subclass

    In the shapes example, add record Triangle(double b, double h) implements Shape { } without changing permits. Read the error. Then add it to permits, and notice area() 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. 3

    Drop the modifier

    In the Vehicle example, remove final from Car. Read sealed, non-sealed or final modifiers expected.

Code & diagrams

A sealed shape family (Java 17 style) Java 17+ New tab
Sign in to run this example in your browser.

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 Rectangle
final, sealed and non-sealed children Java 17+ New tab
Sign in to run this example in your browser.

Expected 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: open
Exhaustive switch over a sealed type Java 21+ New tab

Needs Java 21. Add a fourth record to the permits list and both switches stop compiling until you handle it.

This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

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.0

Break 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.

Main.javawhole filejava
sealed interface Shape permits Circle, Square { }
final class Circle implements Shape { }
final class Square implements Shape { }
final class Triangle implements Shape { }
terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
final class Triangle implements Shape { }
^
1 error

Break #2

Forget final, sealed or non-sealed

Declare a permitted subclass as a plain class.

Main.javawhole filejava
sealed interface Shape permits Circle { }
class Circle implements Shape { }
terminal
$ javac Main.java
── what you'll see ──
Main.java:2: error: sealed, non-sealed or final modifiers expected
class Circle implements Shape { }
^
1 error

Break #3

Miss a case in a switch (Java 21)

Switch over a sealed Shape with a case for Circle only.

Main.javawhole filejava
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) { } }
terminal
$ javac --release 21 Main.java
── what you'll see ──
Main.java:5: error: the switch expression does not cover all possible input values
static double area(Shape s) { return switch (s) { case Circle c -> 3.0 * c.r(); }; }
^
1 error

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

01

Why not an enum?

02

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. permits lists direct children; it says nothing about classes further down a non-sealed branch. Exhaustiveness still works because a case for 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. 1

    Between final (no subclasses) and an ordinary class (anyone can subclass), sealed gives a third option: these subclasses and no others. Write sealed interface Shape permits Circle, Square, Rectangle { }.

  2. 2

    Every permitted subclass must say how *it* continues the hierarchy, with exactly one of: **final (no further subclasses), sealed (its own closed list), or non-sealed** (open again to anyone). Records and enums are implicitly final, so a record can implement a sealed interface with no extra modifier.

  3. 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 permits and the compiler infers the list.

  4. 4

    The payoff is exhaustiveness. Because the compiler knows every subtype, a switch over a sealed type with a case for each subtype needs no default (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. 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 | Cash or Result = Success | Failure.

  6. 6

    It's enforced at run time too. The class file records a PermittedSubclasses attribute, 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

01

What do sealed, permits, final and non-sealed each do?

02

Where must permitted subclasses live?

03

How do sealed types make switch statements safer?

04

How is sealing enforced at run time?

Practice

01

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.

02

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.

03

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, sealed and non-sealed on 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.