Skip to content

Sealed Classes Interview Questions & Answers

14 questions Updated 2026-06-20 Share:

Java sealed classes interview questions — permits clause, sealed vs abstract, permitted subtypes (final/sealed/non-sealed), sealed interfaces, exhaustive switch, algebraic data types, and sealed classes with records.

Read the in-depth guideJava Sealed Classes — Closed Hierarchies, Exhaustive Switch, and ADTs(opens in new tab)
14 of 14

Sealed classes (Java 17, JEP 409) restrict which classes or interfaces may directly extend or implement them using a permits clause. They solve the problem of open inheritance hierarchies where any unknown class can subclass your type, preventing exhaustive analysis by the compiler or by pattern-matching switches.

public sealed interface Shape permits Circle, Rectangle, Triangle {}

Now only Circle, Rectangle, and Triangle can implement Shape. Any other class attempting implements Shape is a compile error.

Rule of thumb: use sealed classes when you own the full set of subtypes and want the compiler to enforce exhaustive handling.

The permits clause lists every allowed direct subtype. If all permitted subtypes are in the same source file as the sealed class, the permits clause may be omitted and the compiler infers it from the file:

// Explicit permits — subtypes may be in separate files:
public sealed class Expr permits Num, Add, Mul {}

// Implicit permits — all in one file:
sealed class Result<T> {
    record Ok<T>(T value)    extends Result<T> {}
    record Err(String msg)   extends Result<Void> {}
}

When subtypes are in different packages or modules, the permits clause is mandatory and the subtypes must be in the same package (for non-modular code) or accessible module.

Rule of thumb: omit permits for concise single-file hierarchies; use explicit permits when subtypes live in separate files.

Every direct subtype of a sealed class/interface must be marked with exactly one of three modifiers:

Modifier Meaning
final Cannot be extended further — closes the hierarchy
sealed Can be extended, but only by its own permits list
non-sealed Opens the hierarchy again — anyone can extend this subtype
sealed interface Notification permits Email, Push, Sms {}

final class Email implements Notification {}    // no further subclasses

sealed class Push implements Notification       // further restricted
    permits AndroidPush, IosPush {}
final class AndroidPush extends Push {}
final class IosPush     extends Push {}

non-sealed class Sms implements Notification {} // open again — anyone can extend
class SmsPremium extends Sms {}                 // allowed

Rule of thumb: use final for leaf types, sealed for multi-level hierarchies, and non-sealed sparingly when you intentionally want to re-open extensibility.

An abstract class restricts instantiation — you can't call new on it — but it places no restriction on subclassing; anyone can extend it. A sealed class restricts who can subclass it — only the permits list — but a sealed class can itself be concrete or abstract.

Aspect abstract class sealed class
Prevents instantiation Yes Only if also abstract
Restricts subclassing No — anyone can extend Yes — only permits list
Compiler exhaustiveness No Yes (with switch pattern matching)
abstract sealed class Vehicle permits Car, Truck {}
// abstract → can't do new Vehicle()
// sealed   → only Car and Truck can extend Vehicle

Rule of thumb: combine abstract sealed when you want both — no direct instances AND a closed subtype set.

Yes. Sealed interfaces work identically to sealed classes but with implements instead of extends. They are particularly useful with records, since records are implicitly final and naturally close the hierarchy:

sealed interface JsonValue
    permits JsonString, JsonNumber, JsonBool, JsonNull, JsonArray, JsonObject {}

record JsonString(String value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonBool(boolean value)  implements JsonValue {}
record JsonNull()               implements JsonValue {}
// JsonArray and JsonObject might be non-record classes

Rule of thumb: sealed interfaces + records are the idiomatic way to model algebraic data types (ADTs) in Java — think Rust enums or Haskell sum types.

Because the compiler knows the complete set of subtypes of a sealed type, a switch expression over that type can be verified as exhaustive at compile time — no default case needed:

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}

double area(Shape s) {
    return switch (s) {              // exhaustive — compiler knows all cases
        case Circle c  -> Math.PI * c.r() * c.r();
        case Square sq -> sq.side() * sq.side();
    };
    // No default needed; adding a new Shape subtype would cause a compile error here
}

If you add a new permitted subtype later, every exhaustive switch that doesn't handle it becomes a compile error — the compiler guides you to all the places that need updating.

Rule of thumb: exhaustive switch over a sealed type is the Java replacement for the visitor pattern — compile-time safety instead of runtime dispatch tables.

Pattern matching in switch (Java 21) gains the most power when used with sealed types because the compiler can check exhaustiveness. You can also deconstruct record components inline:

sealed interface Expr permits Num, Add {}
record Num(int value)       implements Expr {}
record Add(Expr l, Expr r)  implements Expr {}

int eval(Expr e) {
    return switch (e) {
        case Num(int v)         -> v;
        case Add(Expr l, Expr r) -> eval(l) + eval(r);
    };
}

System.out.println(eval(new Add(new Num(1), new Num(2)))); // 3

Rule of thumb: sealed + records + switch pattern matching together model recursive data structures (ASTs, JSON trees, result types) cleanly and safely.

Enums are fixed singletons — each constant is a single shared instance with no per-instance state beyond what's in enum fields. Sealed classes allow each subtype to carry different data and have distinct structure.

Enum Sealed class
Per-instance data Same fields for all constants Each subtype can have different fields
Extensibility Closed (compile time) Closed (compile time)
Exhaustive switch Yes Yes (Java 21)
Polymorphic behaviour Via abstract methods Via subtype methods
// Enum: same structure, different values
enum Status { PENDING, ACTIVE, CLOSED }

// Sealed: different structure per variant
sealed interface Event permits OrderPlaced, OrderShipped, OrderCancelled {}
record OrderPlaced(String orderId, Instant at)         implements Event {}
record OrderShipped(String orderId, String carrier)    implements Event {}
record OrderCancelled(String orderId, String reason)   implements Event {}

Rule of thumb: use enum when all variants are structurally identical; use sealed class when variants carry different data.

Yes. Sealed classes and interfaces can be generic, and permitted subtypes may fix or forward the type parameters:

sealed interface Either<L, R> permits Either.Left, Either.Right {
    record Left<L, R>(L value)  implements Either<L, R> {}
    record Right<L, R>(R value) implements Either<L, R> {}
}

Either<String, Integer> result = new Either.Left<>("error");

String msg = switch (result) {
    case Either.Left<String, Integer>(String s)  -> "Error: " + s;
    case Either.Right<String, Integer>(Integer n) -> "Value: " + n;
};

Rule of thumb: generic sealed types model functional algebraic types like Either, Option, or Result natively in Java without third-party libraries.

Java 17 added Class.permittedSubclasses() which returns an array of ClassDesc descriptors (or use Class.getPermittedSubclasses() which returns Class<?>[] in preview; in standard API it returns ClassDesc[]). In practice most code uses the standard method:

sealed interface Shape permits Circle, Square {}
final record Circle(double r) implements Shape {}
final record Square(double s) implements Shape {}

Class<?>[] permitted = Shape.class.getPermittedSubclasses();
for (Class<?> c : permitted) {
    System.out.println(c.getName());
    // prints: Circle, Square
}

This allows frameworks (serialisation libraries, UI generators) to discover the full closed set of subtypes at runtime without classpath scanning.

Rule of thumb: getPermittedSubclasses() gives you the closed type set at runtime — use it in frameworks to drive deserialization or schema generation without annotation scanning.

Sealed classes are the idiomatic way to represent sum types like Result<T> — a value that is either a success carrying a result or a failure carrying an error:

sealed interface Result<T> permits Result.Ok, Result.Err {
    record Ok<T>(T value)      implements Result<T> {}
    record Err<T>(String msg)  implements Result<T> {}
}

Result<Integer> parse(String s) {
    try { return new Result.Ok<>(Integer.parseInt(s)); }
    catch (NumberFormatException e) { return new Result.Err<>(e.getMessage()); }
}

// Pattern-match at call site:
switch (parse("42")) {
    case Result.Ok<Integer>(int v)  -> System.out.println("Parsed: " + v);
    case Result.Err<Integer>(String m) -> System.err.println("Failed: " + m);
}

Rule of thumb: a sealed Result type makes error paths visible in the type system — callers must handle both cases, unlike checked exceptions which are often swallowed.

non-sealed intentionally re-opens the hierarchy below a sealed type. You'd use it when you own the sealed root (for exhaustive internal dispatch) but want to allow third-party code to extend a specific branch:

sealed interface Plugin permits CorePlugin, ExtensionPlugin {}
final class CorePlugin    implements Plugin {}   // internal, closed
non-sealed class ExtensionPlugin implements Plugin {} // open to third parties

class MyCustomPlugin extends ExtensionPlugin {}  // allowed

The sealed root still gives you a closed set for internal switch expressions that only need to distinguish CorePlugin vs ExtensionPlugin (not their subtypes).

Rule of thumb: use non-sealed to create a deliberate extension point in an otherwise closed hierarchy — document it clearly as a public API contract.

Any class not listed in the permits clause that tries to extend or implement the sealed type gets a compile error:

sealed interface Animal permits Dog, Cat {}
final class Dog implements Animal {}
final class Cat implements Animal {}

// This causes a compile error:
// class Fish implements Animal {}
// error: Fish is not allowed in the sealed hierarchy

// This also fails — permitted subtypes must have final/sealed/non-sealed:
// class Dog implements Animal {}   // error: missing modifier

The compiler also warns on exhaustive switch if a new subtype is added to the permits list but not handled in existing switches.

Rule of thumb: sealed classes turn runtime ClassCastException or missed-case bugs into compile errors — fix them at build time, not in production.

Yes. Several JDK APIs introduced in Java 17–21 use sealed types:

  • java.lang.constant.ConstantDesc — sealed interface with ClassDesc, MethodTypeDesc, DynamicConstantDesc, etc.
  • jdk.incubator.vector — sealed VectorShape hierarchy.
  • Pattern matching in the switch expression itself — the language spec describes case selectors using a sealed hierarchy of pattern kinds.
// ConstantDesc is sealed — you can exhaustively switch over it:
ConstantDesc desc = ...;
switch (desc) {
    case ClassDesc cd       -> ...;
    case MethodTypeDesc md  -> ...;
    // etc.
}

Rule of thumb: look at java.lang.constant for a production-quality example of sealed interfaces in the JDK itself.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel