Explain sealed classes in Java.
Sealed classes, introduced as a preview feature in Java 15 and 16 and standardized in Java 17, allow developers to restrict which classes or interfaces can extend or implement them. This mechanism provides more control over the inheritance hierarchy, making systems more robust and predictable by defining a finite set of known subtypes.
What are Sealed Classes?
A sealed class or interface can explicitly declare which other classes or interfaces are permitted to extend it or implement it. This is a significant step towards managing complexity in large codebases by preventing arbitrary extensions of a type. It enables a type to be both an abstract concept and a closed, enumerable set of implementations.
- Restricted Inheritance: Only explicitly permitted subtypes can extend or implement the sealed type.
- Increased Expressiveness: Clearly communicates the designer's intent about the precise boundaries of the type hierarchy.
- Compiler Assistance: Allows the compiler to perform exhaustive checks in pattern matching with
instanceofandswitchexpressions. - Enhanced Security and Robustness: Prevents unwanted or malicious extensions of core types and ensures all possible concrete types are known.
Syntax
To declare a sealed class or interface, you use the sealed modifier along with the permits clause, which lists the permitted direct subtypes. Each permitted subtype must be declared in the same module as the sealed type (or same package if in an unnamed module).
public abstract sealed class Shape permits Circle, Rectangle, Triangle {
public abstract double area();
}
`permits` keyword
The permits keyword is crucial. It defines the exact set of classes that are allowed to extend or implement the sealed type. If no permits clause is specified, and all permitted subclasses are declared in the same compilation unit, the compiler can infer them. However, it's generally good practice to explicitly list them for clarity.
Rules for Permitted Subtypes
- Location: Permitted subtypes must be in the same module as the sealed class or interface. If the sealed type is in the unnamed module, its permitted subtypes must be in the same package.
- Direct Extension/Implementation: A permitted subtype must directly extend the sealed class or implement the sealed interface.
- Modifier Declaration: Each permitted subtype must explicitly declare how it continues the sealing. It must be declared with one of the following modifiers:
- -
final: Prevents further extension of that subtype. The hierarchy ends here. - -
sealed: Allows further restricted extension of that subtype. It effectively creates a sub-hierarchy that is also sealed. - -
non-sealed: Allows unrestricted extension of that subtype. It breaks the seal, meaning any class can extend it.
Example Scenario: Geometric Shapes
Consider a scenario where you want to model different geometric shapes, but your application will only ever deal with a specific, limited set of shapes. Sealed classes are ideal for this, providing both abstraction and a finite set of implementations.
public abstract sealed class Shape permits Circle, Rectangle, Line {
public abstract String getType();
}
// Circle is a final class, cannot be extended further
public final class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public String getType() { return "Circle"; }
}
// Rectangle is a non-sealed class, can be extended by any class
public non-sealed class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override
public String getType() { return "Rectangle"; }
}
// Line is a sealed class, only specific subtypes can extend it
public sealed class Line extends Shape permits StraightLine, CurvedLine {
private int length;
public Line(int length) { this.length = length; }
@Override
public String getType() { return "Line"; }
}
public final class StraightLine extends Line {
public StraightLine(int length) { super(length); }
@Override
public String getType() { return "Straight Line"; }
}
public final class CurvedLine extends Line {
public CurvedLine(int length) { super(length); }
@Override
public String getType() { return "Curved Line"; }
}
Pattern Matching with `instanceof` and `switch`
Sealed classes work hand-in-hand with pattern matching for instanceof and switch expressions (introduced in Java 16 and enhanced in Java 17). The compiler can now know all possible direct subtypes of a sealed type, allowing it to perform exhaustive checks and often eliminate the need for a default clause in switch expressions.
public String describeShape(Shape shape) {
return switch (shape) {
case Circle c -> "A circle with radius " + c.radius();
case Rectangle r -> "A rectangle with dimensions " + r.width() + "x" + r.height();
case Line l -> "A line of length " + l.length();
// No default case needed if all direct permitted subtypes of Shape are covered.
// If Line had more permitted subtypes, they would need to be covered too or a default would be required for Line.
};
}
Benefits
- Strict Control over Inheritance: Precisely defines the boundaries of a class hierarchy.
- Improved Readability and Maintainability: Developers immediately understand the closed nature of the type hierarchy.
- Enhanced Compiler Safety: Allows for exhaustive
switchexpressions without adefaultcase, catching missing cases at compile time. - Better Tooling Support: IDEs and static analysis tools can leverage this information for more accurate suggestions and warnings.
- Foundation for Future Language Features: Sets the stage for more advanced pattern matching capabilities and data-oriented programming.
Conclusion
Sealed classes in Java provide a powerful mechanism for creating more robust and predictable type hierarchies. By explicitly declaring permitted subtypes, developers gain fine-grained control over inheritance, leading to safer code, clearer intentions, and improved support for modern language features like pattern matching. They are an excellent addition for designing APIs where you want to allow polymorphism but within a well-defined and controlled set of implementations.