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

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

Pro Tips

📋 Quick Reference

ConceptSyntaxNote
Sealed interfacesealed interface S permits A, B {}Closed set of impls
Sealed classsealed class C permits D {}Closed set of subclasses
final subtypefinal class A extends CBranch ends here
sealed subtypesealed class B ... permits ...Keeps restricting
non-sealed subtypenon-sealed class E extends CRe-opens the branch
Record subtyperecord R(...) implements S {}Implicitly final
Omit permitsall subtypes in one fileCompiler infers list
Exhaustive switcharm per subtypeNo default needed
Location rulesame moduleSame package if unnamed
ADT recipesealed + records + switchClosed sum type
SinceJava 17JEP 409

📚 Resources / Keep Learning

🎉 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

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.

Related lessons