4.3 Sealed Classes and Interfaces: permits, final, non-sealed, and Hierarchy Architecture
Key Takeaways
- Sealed classes and interfaces restrict their inheritance hierarchy by explicitly declaring allowed direct subtypes using the sealed keyword and a permits clause.
- Every direct permitted subtype must explicitly declare exactly one modifier: final (closes hierarchy), sealed (continues restricted hierarchy), or non-sealed (opens hierarchy for arbitrary extension).
- In unnamed modules (classpath), permitted subtypes must reside in the exact same package; in named modules, they must reside within the same module across any packages.
- When all permitted subtypes are declared within the same source file (.java compilation unit), the permits clause can be omitted entirely and is inferred by the compiler.
Sealed Classes and Interfaces: permits, final, non-sealed, and Hierarchy Architecture
Standardized in Java 17 (JEP 409) and central to compiler-enforced pattern matching in Java 21, sealed classes and interfaces allow developers to explicitly define and restrict the set of permitted subclasses or implementing types.
Prior to the introduction of sealed types, Java provided only two extremes for inheritance control: completely open inheritance (any class can extend a public class) or completely prohibited inheritance (final class). Developers relied on brittle workarounds like package-private constructors to limit subclassing. Sealed types bridge this architectural gap, enabling domain models to declare closed, well-defined algebraic hierarchies that the compiler can analyze for safety and exhaustiveness.
1. Sealed Class and Interface Declarations
A class or interface is sealed using the sealed modifier accompanied by a permits clause listing all authorized direct subtypes:
// Sealed class permitting exactly three direct subclasses
public sealed class Shape permits Circle, Rectangle, Triangle {
public abstract double area();
}
// Sealed interface permitting two implementing types
public sealed interface PaymentMethod permits CreditCard, BankTransfer {
void processPayment(double amount);
}
Core Rules for the permits Clause
- Direct Subtypes Only: Every type listed in the
permitsclause must directly extend the sealed class (or directly implement the sealed interface). Listing indirect subtypes (e.g. grandchildren) causes a compile-time error. - Bilateral Inheritance Contract: If class
ShapepermitsCircle, thenCirclemust explicitly extendShapein its declaration. IfCirclefails to extendShape, or ifCircleextendsShapewithout being listed inpermits, compilation fails. - Co-location / Accessibility Boundaries:
- In Unnamed Modules (Classpath): The sealed supertype and all its permitted subtypes must reside in the exact same package.
- In Named Modules (JPMS): The sealed supertype and all its permitted subtypes must reside in the same named module (they are permitted to span different packages within that module, provided they are accessible).
2. Mandatory Modifiers for Permitted Subclasses
Every direct permitted subclass of a sealed class must explicitly declare exactly one of the following three modifiers:
┌───> final (Subclassing terminates completely)
│
Permitted Subclass ─┼───> sealed (Continues sealed restriction; must declare permits)
│
└───> non-sealed (Re-opens inheritance hierarchy to any arbitrary class)
// Option 1: final - prevents any further subclassing
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }
}
// Option 2: sealed - continues fine-grained subclass restriction
public sealed class Rectangle extends Shape permits Square, Oblong {
private final double width, height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override public double area() { return width * height; }
}
// Option 3: non-sealed - opens the hierarchy for unrestricted extension
public non-sealed class Triangle extends Shape {
private final double base, height;
public Triangle(double base, double height) { this.base = base; this.height = height; }
@Override public double area() { return 0.5 * base * height; }
}
Critical Subclass Constraints
- Prohibition of Default Modifiers: Omitting all three modifiers (e.g.,
public class Circle extends Shape) causes an immediate compile-time error (class must be sealed, non-sealed or final). - The
non-sealedKeyword:non-sealedis the first hyphenated keyword in Java. It explicitly signals that a subclass opts out of sealed hierarchy restrictions, allowing arbitrary downstream classes to extend it. - Abstract Permitted Classes: If a permitted subclass is declared
abstract, it cannot be markedfinal(becauseabstractandfinalare mutually contradictory). It must be marked eithersealedornon-sealed:public abstract sealed class Polygon extends Shape permits Hexagon, Octagon {}
3. Omitting the permits Clause (Same-File Subclasses)
If all permitted subclasses are declared within the same source file (.java compilation unit) as the sealed supertype (either as nested classes or as peer top-level classes in the same file), the permits clause can be omitted entirely:
// In File: Expr.java
public sealed interface Expr {
// Compiler automatically infers: permits Constant, Add, Multiply
final class Constant implements Expr {
private final int value;
public Constant(int value) { this.value = value; }
public int value() { return value; }
}
final class Add implements Expr {
private final Expr left, right;
public Add(Expr left, Expr right) { this.left = left; this.right = right; }
public Expr left() { return left; }
public Expr right() { return right; }
}
final class Multiply implements Expr {
private final Expr left, right;
public Multiply(Expr left, Expr right) { this.left = left; this.right = right; }
public Expr left() { return left; }
public Expr right() { return right; }
}
}
When javac processes a sealed class or interface lacking a permits clause, it inspects the enclosing compilation unit and automatically derives the list of permitted subtypes.
4. Integration with Records and Enums
Sealed types integrate seamlessly with records and enums to construct clean algebraic data types (ADTs):
1. Records in Sealed Hierarchies
Because records are implicitly final, a record implementing a sealed interface or extending a sealed permits list does not require an explicit final modifier:
public sealed interface JSONValue permits JSONString, JSONNumber, JSONObject, JSONNull {}
public record JSONString(String value) implements JSONValue {} // Implicitly final!
public record JSONNumber(double value) implements JSONValue {} // Implicitly final!
public record JSONObject(Map<String, JSONValue> members) implements JSONValue {} // Implicitly final!
public record JSONNull() implements JSONValue {} // Implicitly final!
2. Enums in Sealed Hierarchies
Enums are implicitly final (or implicitly sealed if they declare constant-specific class bodies). An enum can directly implement a sealed interface:
public sealed interface OperationResult permits SuccessCode, ErrorPayload {}
public enum SuccessCode implements OperationResult {
OK, ACCEPTED, CREATED // Implicitly final!
}
public record ErrorPayload(int code, String message) implements OperationResult {}
5. Reflection API for Sealed Classes
Java SE 21 provides dedicated reflection methods in java.lang.Class to inspect sealed type metadata at runtime:
Class<?> clazz = Shape.class;
// 1. Check if the type is sealed
boolean isSealed = clazz.isSealed(); // Returns true
// 2. Retrieve array of permitted subclasses (returns Class<?>[] or null if not sealed)
Class<?>[] permittedClasses = clazz.getPermittedSubclasses();
for (Class<?> sub : permittedClasses) {
System.out.println("Permitted subtype: " + sub.getName());
}
6. Summary of Key Exam Traps for Sealed Types
| Scenario | Code Example | Outcome / Exam Trap |
|---|---|---|
| Missing Subclass Modifier | class Sub extends SealedBase | Compilation Error: Every direct permitted subclass must be final, sealed, or non-sealed. |
| Abstract Final Subclass | abstract final class Sub extends SealedBase | Compilation Error: abstract and final cannot be combined on any class. |
| Cross-Package Subclassing (Classpath) | Subclass in pkgB extending sealed class in pkgA | Compilation Error: Permitted subtypes on classpath must reside in the exact same package. |
| Missing Permitted Subclass | sealed class A permits B (but B doesn't exist or extend A) | Compilation Error: Bilateral contract requires B to exist and declare extends A. |
| Redundant Final on Record | public final record R() implements SealedI | Valid Syntax: But final is redundant because records are implicitly final. |
| Indirect Subclass in Permits | sealed class A permits GrandChild | Compilation Error: Only direct subclasses may be listed in permits. |
Given the following class declarations in the same package:
What is the result of attempting to compile these classes?public sealed class Transport permits Car, Boat {}
public class Car extends Transport {}
public final class Boat extends Transport {}
Consider the following single compilation unit (MathNode.java):
Why does this code compile successfully despite having no explicit permits clause?public sealed interface MathNode {
record Number(double val) implements MathNode {}
record Negate(MathNode node) implements MathNode {}
}
A developer attempts to declare an abstract subclass extending a sealed superclass:
Which modifier MUST be added at Line X alongside abstract for the code to compile?public sealed class Service permits CloudService, EdgeService {}
// Line X
public abstract class CloudService extends Service {}
Which of the following statements is TRUE regarding records implementing sealed interfaces?