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.
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
permitskeyword follows anyextendsandimplementsclauses. - It lists the simple or fully qualified names of all authorized direct subclasses, separated by commas.
- Subclasses listed in the
permitsclause 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:
final: Prohibits any further extension. This terminates the inheritance branch completely.sealed: Extends the sealed superclass and continues sealing by specifying its own authorized subclasses via a newpermitsclause.non-sealed: Explicitly re-opens the inheritance hierarchy. Any arbitrary class can extend this subclass (note the hyphenated keywordnon-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:
sealed: Continues sealing with its own permitted types.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:
- The compiler still enforces the hierarchy: a class that is not listed in
permits(or not in the same file whenpermitsis omitted) cannot extendResult. - A Java 17
if/elsechain is not checked for exhaustiveness. If a new permitted subtype is added, nothing forces this method to change, so keep a final fallback branch.
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?
Compilation fails because Spreadsheet cannot be final when extending a sealed class.
Both classes compile successfully because Report defaults to non-sealed.
Compilation fails because Report must be declared with exactly one of final, sealed, or non-sealed.
Compilation fails because Document must provide concrete method implementations.
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?
RouterNode cannot use the non-sealed modifier.
NetworkNode cannot permit sub-interfaces, only classes.
Permitted sub-interfaces must belong to the java.net package.
SwitchNode fails compilation because interfaces cannot be declared final.
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?
Compilation fails only at Oval, because Circle is final.
Compilation fails at both Tile and Oval, because they are not listed in Shape's permits clause.
The code compiles, because every class is in the same package as Shape.
Compilation fails only at Tile, because a non-sealed class cannot be extended.
Sections you finish are checked off in the contents.