Java 17 Sealed Classes: A Practical Engineering Decision
Java 17 sealed classes let you restrict subclassing to a finite set. This guide shows how to declare a sealed hierarchy, use exhaustive pattern matching, and avoid common pitfalls. Compile with <code>javac --release 17</code> and keep the <code>permits</code> list in sync for reliable type safety.
06 Jul 2026, 10:03 UTC

Why Sealed Classes Matter for Your Codebase
When you need a type that can only be extended by a known set of classes, sealed classes give you compile‑time enforcement. They let the compiler know the exact subclasses, which improves type safety, enables exhaustive switch statements, and documents intent for future maintainers.
Defining a Sealed Class Hierarchy
Use the permits clause to list every allowed subclass. The syntax is available from Java 17 onward; you must compile with --release 17 (or -source 17 -target 17) so the compiler recognises the feature.
// Shape.java
public sealed class Shape permits Circle, Rectangle, Polygon {
public abstract double area();
}
// Circle.java
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; }
}
// Rectangle.java
public final class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width; this.height = height;
}
@Override public double area() { return width * height; }
}
// Polygon.java – a non‑sealed extension that can grow later
public non‑sealed class Polygon extends Shape {
private final int sides;
public Polygon(int sides) { this.sides = sides; }
@Override public double area() { return 0; /* placeholder */ }
}
Notice the final modifier on Circle and Rectangle—they cannot be subclassed further. Polygon is declared non‑sealed, allowing future extensions without altering the parent’s permits list.
Exhaustive Switch with Pattern Matching
With a sealed hierarchy, the compiler can verify that a switch covers all possible cases. This is a powerful safety net when adding new subclasses.
public static double totalArea(List<Shape> shapes) {
return shapes.stream()
.mapToDouble(s -> switch (s) {
case Circle c -> c.area();
case Rectangle r -> r.area();
case Polygon p -> p.area();
})
.sum();
}
Compiling this with javac --release 17 will succeed. If you add a new sealed subclass, the compiler will flag the switch as non‑exhaustive, forcing you to handle the new case.
Limitations and Edge Cases
- Generic Wildcards: You can use
List<? extends Shape>butList<? super Shape>cannot contain non‑sealed subclasses because the compiler cannot guarantee the type safety of the wildcard. - Reflection: The sealed constraint is enforced at compile time. Using reflection to create subclasses bypasses the rule, but the resulting classes will not be assignable to
Shapewithout apermitsdeclaration. - Inter‑module Boundaries: In a multi‑module project, each module must declare the permitted subclasses in its own module-info. The compiler checks the
permitslist across module boundaries. - Preview Features: If you compile with
--enable-preview(pre‑Java 17), you need to run with the same flag. From Java 17 onward, sealed classes are a stable feature.
Common Pitfalls and How to Avoid Them
- Missing the
permitsClause: A sealed class without apermitslist is illegal. Always include the clause, even if you currently have no subclasses. - Forgetting to Declare All Subclasses: If you add a new subclass and forget to update the
permitslist, compilation will fail. Keep the list in sync or use a code‑review checklist. - Mixing Sealed and Non‑Sealed Subclasses Unnecessarily: If you only need a fixed set of subclasses, make them all
final. Introducingnon‑sealedsubclasses defeats the purpose of restriction. - Using Wildcard Generics Improperly: Avoid
List<? super Shape>when you need to add non‑sealed subclasses. Stick toList<Shape>orList<? extends Shape>for read‑only collections. - Assuming Runtime Enforcement: Sealed classes are compile‑time only. If you load classes dynamically, the sealed constraint is not enforced by the JVM.
Practical Checklist for Introducing Sealed Classes
- Verify your project targets Java 17 or newer.
- Use
javac --release 17or configure your build tool accordingly. - Define the sealed class with a
permitsclause. - Mark all concrete subclasses as
finalunless future extension is required. - Update switch statements to be exhaustive; run
javacto catch missing cases. - Run unit tests that cover each subclass path.
- Document the intent in code comments and architecture notes.
Sealed classes are a straightforward, type‑safe tool for controlling inheritance. When used correctly, they reduce bugs, clarify intent, and empower the compiler to catch missing branches before runtime.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.