4.4 Sealed Classes and Interfaces: `permits`, Hierarchy Constraints, and Modifiers

Key Takeaways

  • Sealed classes and interfaces restrict which subtypes can extend or implement them using the sealed keyword and permits clause.

  • Every permitted subclass must directly extend the sealed class, reside in the same package (or same named module), and declare exactly one modifier: final, sealed, or non-sealed.

  • When all permitted subclasses are declared within the same compilation unit (.java file) as the sealed class, the permits clause can be omitted.

  • Sealed interfaces can permit both implementing classes and sub-interfaces; sub-interfaces can only be declared as sealed or non-sealed because interfaces cannot be final.

  • In standard Java 17, pattern-matching switch over a sealed type is a disabled preview feature (JEP 406); sealed hierarchies are instead tested with instanceof patterns, and exhaustive sealed switches became standard only in Java 21.

Last updated: October 2026

Sealed Classes and Interfaces: permits, Hierarchy Constraints, and Modifiers

Prior to Java 17, object-oriented design in Java forced developers into an all-or-nothing inheritance dilemma: a class could be declared final (preventing any subclassing) or left non-final (allowing arbitrary extension by any class in the package or classpath). Sealed classes and interfaces (finalized in Java 17 under JEP 409) solve this problem by enabling fine-grained, declarative inheritance boundaries. A sealed type specifies exactly which subclasses or sub-interfaces are authorized to extend or implement it.


1. Syntax of Sealed Classes and the permits Clause

A sealed class is declared by prefixing the class keyword with sealed, followed by a permits clause listing authorized direct subclasses:

public abstract sealed class Shape permits Circle, Rectangle, Triangle {
    public abstract double area();
}

The permits Clause

  • The permits keyword follows any extends and implements clauses.
  • It lists the simple or fully qualified names of all authorized direct subclasses, separated by commas.
  • Subclasses listed in the permits clause must be accessible to the sealed class at compile time.
public sealed class Account extends Entity implements Auditable permits SavingsAccount, CheckingAccount {}

2. The Three Constraints on Permitted Subclasses

Every permitted subclass must satisfy three strict rules defined in the Java Language Specification:

Constraint 1: Direct Extension

Every permitted subclass must directly extend the sealed superclass (or directly implement the sealed interface). An indirect subclass cannot appear in the superclass's permits clause.

Constraint 2: Package and Modular Colocation

Permitted subclasses must reside within the same scoping boundary as the sealed class:

  • In Named JPMS Modules: The sealed class and all its permitted subclasses must belong to the same named module, though they may reside in different packages within that module.
  • In Unnamed Modules (Traditional Classpath): The sealed class and all its permitted subclasses must belong to the exact same package.

Warning

If a sealed class in package com.exam.finance permits a subclass in package com.exam.security while both are on the classpath (the unnamed module), compilation fails: in the unnamed module, a sealed class and its permitted subclasses must share the same package.

Constraint 3: The Subclass Modifier Triad (final, sealed, non-sealed)

Every direct permitted subclass must explicitly declare exactly ONE of three modifiers:

  1. final: Prohibits any further extension. This terminates the inheritance branch completely.
  2. sealed: Extends the sealed superclass and continues sealing by specifying its own authorized subclasses via a new permits clause.
  3. non-sealed: Explicitly re-opens the inheritance hierarchy. Any arbitrary class can extend this subclass (note the hyphenated keyword non-sealed).
public abstract sealed class Transport permits Car, Train, Aircraft {}

// Option 1: Terminate extension with final
public final class Car extends Transport {}

// Option 2: Continue sealing with sealed
public sealed class Train extends Transport permits FreightTrain, BulletTrain {}
public final class FreightTrain extends Train {}
public final class BulletTrain extends Train {}

// Option 3: Re-open inheritance with non-sealed
public non-sealed class Aircraft extends Transport {}
public class Drone extends Aircraft {} // Legal: Aircraft is non-sealed

If a permitted subclass omits all three modifiers (e.g., public class Car extends Transport {}), compilation fails with: sealed, non-sealed, or final modifiers expected.


3. Omitting the permits Clause (Same-File Rule)

If all permitted subclasses are declared within the same compilation unit (.java source file) as the sealed superclass, the permits clause is optional.

// File: Expression.java
public sealed class Expression { // No permits clause!
    // ...
}

final class ConstantExpr extends Expression {}
final class BinaryExpr extends Expression {}

When permits is omitted, the compiler inspects the file, discovers all classes extending Expression, and generates an implicit permits ConstantExpr, BinaryExpr clause. If another class in a different .java file attempts to extend Expression, the compiler rejects it: class is not allowed to extend sealed class: Expression.


4. Sealed Interfaces

Interfaces can also be declared as sealed. A sealed interface can permit both implementing classes and sub-interfaces:

public sealed interface Service permits LocalService, RemoteService, MockService {}

Sub-Interface Rules: Interfaces Cannot Be final!

Because Java interfaces cannot be marked final (interfaces are fundamentally abstract types meant to be implemented), a permitted sub-interface can only be declared with one of two modifiers:

  1. sealed: Continues sealing with its own permitted types.
  2. non-sealed: Re-opens the interface so any interface or class can implement/extend it.
// Permitted sub-interfaces:
public sealed interface RemoteService extends Service permits CloudService {}
public non-sealed interface LocalService extends Service {}
// public final interface BadService extends Service {} // COMPILE ERROR: modifier final not allowed here

// Permitted implementing class:
public final class MockService implements Service {}

Records Implementing Sealed Interfaces

Records are implicitly final. Therefore, when a record implements a sealed interface, it automatically satisfies the final requirement without needing the explicit final keyword:

public sealed interface Token permits StringToken, IntToken {}

public record StringToken(String value) implements Token {} // Valid: Record is implicitly final
public record IntToken(int value) implements Token {}       // Valid: Record is implicitly final

5. Sealed Types and Pattern Matching in Java 17

A long-term motivation for sealed types is algebraic data modeling: because the compiler knows every permitted subtype, it can check that code handles all of them. In Java 17, however, the switch form of that check is not standard Java:

  • Pattern matching for switch (case Success s ->) is a preview feature in Java 17 (JEP 406). Without --enable-preview, javac 17 rejects it: patterns in switch statements are a preview feature and are disabled by default.
  • It became a standard feature in Java 21 (JEP 441). Exhaustive sealed switches therefore belong to the 1Z0-830 (Java 21) objectives, not to standard Java 17 code.

In standard Java 17, test a sealed hierarchy with pattern matching for instanceof (final since Java 16):

public sealed interface Result permits Success, Failure, Pending {}
public final class Success implements Result { public String data() { return "OK"; } }
public final class Failure implements Result { public String error() { return "ERR"; } }
public final class Pending implements Result {}

public class Dispatcher {
    public static String inspect(Result res) {
        if (res instanceof Success s) return "Data: " + s.data();
        if (res instanceof Failure f) return "Error: " + f.error();
        return "Awaiting response";   // Only Pending remains, but the compiler does not prove it
    }
}

Two exam-relevant consequences follow:

  1. The compiler still enforces the hierarchy: a class that is not listed in permits (or not in the same file when permits is omitted) cannot extend Result.
  2. A Java 17 if/else chain is not checked for exhaustiveness. If a new permitted subtype is added, nothing forces this method to change, so keep a final fallback branch.
Test Your Knowledge

Examine the following class declarations:

public sealed class Document permits Report, Spreadsheet {}

public class Report extends Document {}
public final class Spreadsheet extends Document {}

What is the compilation result?

A

Compilation fails because Spreadsheet cannot be final when extending a sealed class.

B

Both classes compile successfully because Report defaults to non-sealed.

C

Compilation fails because Report must be declared with exactly one of final, sealed, or non-sealed.

D

Compilation fails because Document must provide concrete method implementations.

Test Your Knowledge

Consider the following sealed interface and sub-types:

public sealed interface NetworkNode permits SwitchNode, RouterNode {}

public final interface SwitchNode extends NetworkNode {}
public non-sealed interface RouterNode extends NetworkNode {}

Why does this code fail to compile?

A

RouterNode cannot use the non-sealed modifier.

B

NetworkNode cannot permit sub-interfaces, only classes.

C

Permitted sub-interfaces must belong to the java.net package.

D

SwitchNode fails compilation because interfaces cannot be declared final.

Test Your Knowledge

Given the following declarations compiled together on the classpath with javac 17:

public sealed class Shape permits Circle, Square {}
final class Circle extends Shape {}
non-sealed class Square extends Shape {}
class Tile extends Square {}
class Oval extends Circle {}

Which statement is correct?

A

Compilation fails only at Oval, because Circle is final.

B

Compilation fails at both Tile and Oval, because they are not listed in Shape's permits clause.

C

The code compiles, because every class is in the same package as Shape.

D

Compilation fails only at Tile, because a non-sealed class cannot be extended.

Sections you finish are checked off in the contents.