Sealed Classes & Interfaces
Reviewed & published by Brayan K
A sealed class or interface restricts which other types may extend or implement it to an explicit permits list, giving you a closed, compiler-known hierarchy you can switch over exhaustively.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should be comfortable with inheritance, records, and pattern matching for switch. Sealed types are standard from Java 17.
What You'll Learn
- Declaring a sealed type with permits
- The final / sealed / non-sealed rule for subclasses
- Exhaustive switches with no default
- How adding a subtype turns into a compile error
- Modelling algebraic data types
- When you can omit the permits clause
The Big Idea — A Guest List, Not an Open Door
💡 Analogy: A normal interface is a party with the door wide open — anyone may walk in and implement it. A final class is a locked room nobody can enter. A sealed type is a party with a guest list: only the names on permits get in. Because you know exactly who's at the party, you can plan for everyone — that's why a switch over a sealed type can be complete with no "and anyone else" fallback.
A closed set of possibilities is enormously powerful: the compiler can now prove you've handled every case, and warn you the moment a new guest is added.
1️⃣ sealed + permits + Exhaustive switch
Declare the parent sealed and list its implementors with permits. Then a switch that has an arm per permitted subtype is exhaustive — no default required. Records make ideal subtypes because they are implicitly final and carry their data plainly.
2️⃣ final, sealed, and non-sealed Subclasses
Every direct subclass must say how it continues the seal. final closes the branch. sealed keeps restricting with its own permits. non-sealed reopens the branch for unrestricted extension. You must pick one — the compiler won't let a permitted subclass stay ambiguously open.
public class Main {
// 'permits' restricts the subclasses. Each subclass picks a modifier:
sealed static class Vehicle permits Car, Truck, SportsCar {
final String name;
Vehicle(String name) { this.name = name; }
}
// final: no further subclassing allowed
static final class Car extends Vehicle {
Car() { super("Car"); }
}
// sealed: continues the restriction to its own permitted subclass
sealed static class Truck extends Vehicle permits PickupTruck {
Truck(String name) { super(name); }
}
final static class PickupTruck extends Truck {
PickupTruck() { super("PickupTruck"); }
}
// non-sealed: deliberately re-opens for anyone to extend
non-sealed static class SportsCar extends Vehicle {
SportsCar() { super("SportsCar"); }
}
public static void main(String[] args) {
Vehicle[] vs = { new Car(), new PickupTruck(), new SportsCar() };
for (Vehicle v : vs) {
System.out.println(v.name + " is a " + v.getClass().getSimpleName());
}
}
}3️⃣ Modelling Algebraic Data Types
A sealed parent plus record subtypes models an algebraic data type: a value that is exactly one of a fixed set of shapes. The classic example is an expression tree — an Expr is a number, an addition, or a multiplication — evaluated with a single exhaustive, recursive switch.
public class Main {
// An algebraic data type: an Expr is EXACTLY one of these shapes.
sealed interface Expr permits Num, Add, Mul {}
record Num(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
// Because Expr is sealed, this switch needs no default — the compiler
// knows the three cases are all of them.
static int eval(Expr e) {
return switch (e) {
case Num n -> n.value();
case Add a -> eval(a.left()) + eval(a.right());
case Mul m -> eval(m.left()) * eval(m.right());
};
}
public static void main(String[] args) {
// (2 + 3) * 4
Expr tree = new Mul(new Add(new Num(2), new Num(3)), new Num(4));
System.out.println("(2 + 3) * 4 = " + eval(tree));
// 10 + (5 * 6)
Expr tree2 = new Add(new Num(10), new Mul(new Num(5), new Num(6)));
System.out.println("10 + (5 * 6) = " + eval(tree2));
}
}🧠 Quick Recall
Predict the result before opening each answer.
sealed interface A permits B {}
class B implements A {}Answer: No. B is a permitted subtype but doesn't declare final, sealed, or non-sealed. error: sealed, non-sealed or final modifiers expected. Mark B as one of them.
sealed interface S permits X, Y {}
record X() implements S {}
record Y() implements S {}
int f(S s) {
return switch (s) {
case X x -> 1; // Y is missing!
};
}Answer: It won't compile — the switch isn't exhaustive because Y isn't handled. The compiler reports the missing case. Add case Y y -> 2;.
sealed interface S permits X {}
record X() implements S {}
class Rogue implements S {}Answer: No. Rogue isn't on the permits list, so error: Rogue is not allowed in the sealed interface S. Only listed types may implement a sealed interface.
🎯 Your Turn — A sealed Payment type
Model three payment kinds and describe each with an exhaustive switch (no default).
public class Main {
// YOUR TURN: model a Payment that is EXACTLY one of three kinds.
//
// 1. sealed interface Payment permits Cash, Card, Voucher {}
// 2. record Cash(double amount) implements Payment {}
// record Card(double amount, String last4) implements Payment {}
// record Voucher(String code) implements Payment {}
// 3. A describe(Payment) using an exhaustive switch (NO default):
// Cash -> "Cash of $" + amount
// Card -> "Card ending " + last4 + " for $" + amount
// Voucher -> "Voucher " + code
//
// Expected output:
// Cash of $20.0
// Card ending 1234 for $50.0
// Voucher FREE5
public static void main(String[] args) {
// Payment[] ps = { new Cash(20.0), new Card(50.0, "1234"), new Voucher("FREE5") };
// for (Payment p : ps) System.out.println(describe(p));
}
}🧩 Mini-Challenge — A JSON value ADT
Build a closed set of JSON value variants and render each one exhaustively.
public class Main {
// MINI-CHALLENGE: a tiny JSON value type as an algebraic data type.
//
// sealed interface Json permits JNull, JBool, JNum, JStr {}
// record JNull() implements Json {}
// record JBool(boolean b) implements Json {}
// record JNum(double n) implements Json {}
// record JStr(String s) implements Json {}
//
// Write render(Json) with an EXHAUSTIVE switch:
// JNull -> "null"
// JBool -> "true"/"false"
// JNum -> the number
// JStr -> the string wrapped in double quotes
//
// Expected output:
// null
// true
// 3.5
// "hi"
public static void main(String[] args) {
// Json[] vals = { new JNull(), new JBool(true), new JNum(3.5), new JStr("hi") };
// for (Json j : vals) System.out.println(render(j));
}
}Common Errors
- ❌ A permitted subclass with no modifier. error: sealed, non-sealed or final modifiers expected. Fix: mark it final, sealed, or non-sealed.
- ❌ An unlisted class implementing the sealed type. error: not allowed in the sealed interface. Fix: add it to permits (or rethink the design).
- ❌ Permitted subtype in another package (unnamed module). Fix: keep them in the same package/module.
- ❌ Non-exhaustive switch over a sealed type. Fix: handle every permitted subtype (or add default).
- ❌ Adding default and losing the safety net. A new subtype no longer triggers a compile error. Fix: prefer explicit cases for closed hierarchies.
- ❌ Forgetting records are already final. You can't extend a record. Fix: use composition or a sealed interface instead.
Pro Tips
- 💡 Sealed interface + records is the go-to recipe for modelling a fixed set of cases (results, events, AST nodes).
- 💡 Omit permits for small file-local hierarchies — if all subtypes are in the same file, the compiler infers the list.
- 💡 Avoid default on closed hierarchies. Let the compiler flag every switch when you add a case.
- 💡 Use non-sealed sparingly. It reopens the hierarchy and gives up the exhaustiveness guarantee for that branch.
📋 Quick Reference
| Concept | Syntax | Note |
|---|---|---|
| Sealed interface | sealed interface S permits A, B {} | Closed set of impls |
| Sealed class | sealed class C permits D {} | Closed set of subclasses |
| final subtype | final class A extends C | Branch ends here |
| sealed subtype | sealed class B ... permits ... | Keeps restricting |
| non-sealed subtype | non-sealed class E extends C | Re-opens the branch |
| Record subtype | record R(...) implements S {} | Implicitly final |
| Omit permits | all subtypes in one file | Compiler infers list |
| Exhaustive switch | arm per subtype | No default needed |
| Location rule | same module | Same package if unnamed |
| ADT recipe | sealed + records + switch | Closed sum type |
| Since | Java 17 | JEP 409 |
📚 Resources / Keep Learning
- 📘 Oracle: Sealed Classes and Interfaces — the official language guide.
- 📗 dev.java: Sealed Classes — a guided tutorial.
- 📙 JEP 409: Sealed Classes — the finalizing proposal and rationale.
🎉 Lesson Complete!
You can now seal a hierarchy with permits, choose final/sealed/non-sealed for each subtype, write exhaustive switches with no default, and model algebraic data types with sealed interfaces and records.
Practice quiz
What does a sealed class or interface do?
- Makes a class final
- Hides all its methods
- Restricts which classes may extend or implement it
- Prevents instantiation
Answer: Restricts which classes may extend or implement it. sealed limits the permitted subtypes to an explicit list, controlling the type hierarchy.
Which keyword lists the allowed subtypes?
- permits
- allows
- extends
- implements
Answer: permits. sealed interface Shape permits Circle, Square {} names the permitted implementors.
What modifiers must a permitted subclass declare?
- public or private
- abstract only
- static
- final, sealed, or non-sealed
Answer: final, sealed, or non-sealed. Every direct subclass of a sealed type must be final, sealed (with its own permits), or non-sealed.
What does non-sealed do?
- Makes it final
- Re-opens a type so anyone can extend it again
- Hides it
- Deletes the permits list
Answer: Re-opens a type so anyone can extend it again. non-sealed deliberately lifts the restriction for that branch, allowing unrestricted further subclassing.
In which Java version did sealed classes become standard?
- Java 17
- Java 11
- Java 14
- Java 21
Answer: Java 17. Sealed classes finalized in Java 17 (JEP 409).
Why is a switch over a sealed type exhaustive without default?
- Switches never need default
- Sealed disables default
- The compiler knows the complete set of permitted subtypes
- It uses reflection
Answer: The compiler knows the complete set of permitted subtypes. Because permits lists every subtype, the compiler can verify all cases are covered.
Can a permitted subclass live in a different package from the sealed parent (unnamed module)?
- Always
- It must be in the same module, and same package if in an unnamed module
- Never
- Only if final
Answer: It must be in the same module, and same package if in an unnamed module. Permitted subclasses must be in the same module; in the unnamed module they must share the package.
What happens if you omit a permitted case in an exhaustive switch and later add a subtype?
- Nothing
- It silently returns null
- Runtime crash only
- The switch becomes non-exhaustive and the compiler flags it
Answer: The switch becomes non-exhaustive and the compiler flags it. Adding a permitted subtype makes existing exhaustive switches incomplete, so the compiler points you to each one.
What is an 'algebraic data type' modelled with sealed + records?
- A math library
- A type that is exactly one of a fixed set of shapes
- A generic collection
- A thread pool
Answer: A type that is exactly one of a fixed set of shapes. Sealed parent + record subtypes expresses a closed sum type: a value is exactly one of the listed variants.
Can you omit permits when the subclasses are in the same source file?
- No, permits is always required
- Only for interfaces
- Yes — the compiler infers permitted types from that file
- Only for records
Answer: Yes — the compiler infers permitted types from that file. If all permitted subtypes are declared in the same compilation unit, you may omit the permits clause.
Continue this course
- Previous: Checkpoint: Objects & Data
- Next: Record Patterns — Deconstruct records directly in switch and instanceof (Java 21)
- Quick reference: Java cheat sheet
Frequently asked questions
What problem do sealed classes solve?
Before sealed types you had two extremes: a final class that nobody can extend, or an open class/interface that anybody can extend. Sealed types sit in between: you declare a fixed, known set of permitted subtypes. This lets you model a closed set of possibilities — 'a Shape is exactly a Circle, Square, or Rectangle' — and the compiler can then reason about completeness, for example proving a switch covers every case. It is the missing tool for designing a deliberately closed hierarchy.
What must each permitted subclass declare?
Every direct subclass of a sealed type must itself choose one of three modifiers. final means it cannot be extended further (the most common choice, and records are implicitly final). sealed means it continues the restriction with its own permits list. non-sealed deliberately re-opens that branch so anyone may extend it again. The compiler enforces that you pick one — you cannot leave a permitted subclass 'open' by accident.
Why can a switch over a sealed type skip default?
Because the permits clause enumerates every possible subtype, the compiler knows the complete set of cases. If your switch has an arm for each permitted subtype, it is provably exhaustive and no default is needed. The real payoff comes later: if you add a new permitted subtype, every exhaustive switch that doesn't handle it stops compiling, and the compiler points you to each place you must update. That turns a whole class of 'I forgot to handle the new case' bugs into compile errors.
Where must permitted subclasses live?
Permitted subtypes must be in the same module as the sealed type. If everything is in the unnamed module (a typical small project without module-info.java), they must also be in the same package. You can list them explicitly with permits, or — if all the subtypes are declared in the same source file — omit permits and let the compiler infer them, which is handy for small closed hierarchies written in one file.
How do sealed types relate to records and pattern matching?
They are designed to work together as 'data-oriented programming'. A sealed interface defines the closed set of cases; records define each case as immutable data; and a pattern switch dispatches over them exhaustively. Together they let you express an algebraic data type — a value that is exactly one of several shapes — and process it with a single, compiler-checked switch. The next lesson, record patterns, takes this further by deconstructing those records inside the switch.