Sealed Classes
IntermediateSealed classes restrict which classes can extend or implement them, enabling exhaustive pattern matching and closed hierarchies.
Overview
Sealed classes, introduced in Java 17, let you explicitly declare which classes or interfaces are permitted to extend or implement a type. This creates a closed hierarchy that the compiler can reason about exhaustively — enabling complete switch expressions and pattern matching. Sealed classes combine well with records and patterns to model algebraic data types cleanly in Java.
Declaring Sealed Classes
A sealed class uses the sealed modifier and a permits clause listing allowed subtypes. Each permitted subtype must be in the same package (or module) and must declare itself as final, sealed, or non-sealed.
final means no further extension. sealed means it further restricts its own subtypes. non-sealed reopens the hierarchy — any class can extend it.
// Sealed hierarchy for shapes
public sealed class Shape
permits Circle, Rectangle, Triangle {}
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
public double radius() { return radius; }
}
public final class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width; this.height = height;
}
public double width() { return width; }
public double height() { return height; }
}
public non-sealed class Triangle extends Shape {
// can be extended freely
}Sealed Interfaces
Interfaces can also be sealed. This is particularly useful for modelling command or result types where you want an exhaustive set of implementations.
Combining sealed interfaces with records gives you a concise algebraic data type (ADT) style that is very common in functional languages.
public sealed interface Result<T>
permits Result.Success, Result.Failure {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
}
// Usage
Result<Integer> r = new Result.Success<>(42);
String msg = switch (r) {
case Result.Success<Integer> s -> "Value: " + s.value();
case Result.Failure<Integer> f -> "Error: " + f.error();
};Exhaustive Switch with Sealed Types
When you switch over a sealed type and cover every permitted subtype, the compiler knows the switch is exhaustive — no default branch needed. This is one of the main motivations for sealed classes.
If you add a new permitted type later, all exhaustive switches become compile errors, guiding you to handle the new case.
public double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed — compiler knows all subtypes
};
}Key Points to Remember
- sealed restricts which classes can extend or implement a type using permits.
- Permitted subtypes must be final, sealed, or non-sealed.
- Sealed hierarchies enable exhaustive switch expressions without a default.
- Sealed interfaces + records = concise algebraic data types in Java.
- All permitted subtypes must reside in the same package or module.
Practice Sealed Classes in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat problem do sealed classes solve that was previously handled with package-private constructors?
What is the difference between final, sealed, and non-sealed subclasses?
How do sealed classes improve switch expression exhaustiveness?
Can a record implement a sealed interface?
What restrictions apply to the location of permitted subtypes?
Ask Aria about Sealed Classes
Your personal AI tutor — ask anything about this concept