Java 17 introduced sealed classes as part of the enhancements to the type system. Sealed classes allow developers to explicitly control which classes can extend or implement a class or interface, thereby achieving better type safety and making it easier to design domain-specific hierarchies. Here’s a guide on how to use sealed classes effectively:
What are Sealed Classes?
Sealed classes restrict which other classes or interfaces can extend or implement them. By using sealed classes, you can:
- Define a closed hierarchy of types where only a fixed set of subtypes is allowed.
- Ensure better maintainability and readability of your type hierarchy.
- Provide exhaustive handling for these types with features like
switch
statements.
The syntax revolves around the sealed
, non-sealed
, and final
keywords.
Declaring and Using Sealed Classes
1. Declaration
To declare a sealed class:
- Use the
sealed
modifier.
- Specify the permitted subclasses with the
permits
clause.
package org.kodejava.basic;
public sealed class Shape permits Circle, Rectangle, Square {
// Common properties and methods for all shapes
}
In this example:
Shape
is the sealed class.
- Only
Circle
, Rectangle
, and Square
are allowed to extend Shape
.
2. Permitted Subclasses
Every subclass permitted by the sealed
class must opt for one of the following:
final
: The subclass cannot be further extended.
non-sealed
: The subclass can be extended by any other class.
sealed
: The subclass restricts its hierarchy further with permits
.
Examples:
package org.kodejava.basic;
// Final subclass (cannot have further subclasses)
public final class Circle extends Shape {
double radius;
public Circle(double radius) {
this.radius = radius;
}
}
package org.kodejava.basic;
// Sealed subclass with its own permitted subclasses
public sealed class Rectangle extends Shape permits RoundedRectangle {
double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
}
package org.kodejava.basic;
// Non-sealed subclass (can have arbitrary subclasses)
public non-sealed class Square extends Shape {
double side;
public Square(double side) {
this.side = side;
}
}
package org.kodejava.basic;
// Permitted subclass of Rectangle
public final class RoundedRectangle extends Rectangle {
double cornerRadius;
public RoundedRectangle(double width, double height, double cornerRadius) {
super(width, height);
this.cornerRadius = cornerRadius;
}
}
Benefits of Sealed Classes
- Closed Type Hierarchies
Sealed classes provide an explicit way to define and restrict type hierarchies, avoiding unintended subclasses.
-
Exhaustiveness in switch
Statements
When all subclasses of a sealed class are known, the compiler ensures exhaustiveness in switch
expressions. This helps eliminate the possibility of missing a case.
Example:
public double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
case RoundedRectangle rr ->
rr.width * rr.height - (4 - Math.PI) * rr.cornerRadius * rr.cornerRadius / 4;
case Rectangle r -> r.width * r.height;
case Square s -> s.side * s.side;
default -> throw new IllegalStateException("Unexpected value: " + shape);
};
}
If you later add a new subclass to Shape
, the compiler will generate an error until you update the switch statement accordingly.
-
Immutability and Security
By marking direct subclasses as final
or controlling further inheritance (e.g., via sealed
vs. non-sealed
), you ensure immutability in specific contexts and prevent unintended behavior caused by subclassing.
Practical Use Cases for Sealed Classes
-
Domain Modelling
Example: A sealed class Payment
can have subclasses for CreditCardPayment
, BankTransfer
, and CryptoPayment
.
public sealed class Payment permits CreditCardPayment, BankTransfer, CryptoPayment {
// Common payment attributes
}
public final class CreditCardPayment extends Payment {
// Credit card specific fields
}
public final class BankTransfer extends Payment {
// Bank transfer specific fields
}
public final class CryptoPayment extends Payment {
// Crypto payment specific fields
}
- Compiler Assistance for Type-Safe Code
The sealed hierarchy ensures that when you process these types (e.g., with switch
or polymorphic methods), the compiler helps enforce exhaustive handling.
Key Points to Remember
- You must list all permitted subclasses explicitly using the
permits
clause.
- All subclasses of a sealed class must be declared in the same module or package as the sealed class (enhanced encapsulation).
sealed
, non-sealed
, and final
define the inheritance relation for permitted subclasses.
Summary
Sealed classes are a powerful tool in Java 17 for creating controlled and predictable type hierarchies. They help enforce constraints at compile-time, reduce runtime errors, and assist developers in creating clean, maintainable, and type-safe code. Use them effectively to create robust domain models and application logic.