Sealed Classes, finalized in Java 17 through JEP 409, introduce a powerful mechanism for controlling inheritance in Java. This feature allows you to explicitly declare which classes are permitted to extend or implement a sealed type, giving you unprecedented control over your class hierarchies.
What Are Sealed Classes?
A sealed class or interface restricts which other classes or interfaces may extend or implement it. You declare a class as sealed and use the permits clause to specify the allowed subclasses.
public sealed class Shape permits Circle, Rectangle, Triangle {
// Common shape behavior
}
In this example, only Circle, Rectangle, and Triangle are allowed to extend Shape. Any attempt by another class to extend Shape will result in a compile-time error.
Why Use Sealed Classes?
1. Domain Modeling
Sealed classes are perfect for modeling closed hierarchies where you know all possible subtypes in advance.
public sealed class Vehicle permits Car, Truck, Motorcycle {
private final String licensePlate;
public Vehicle(String licensePlate) {
this.licensePlate = licensePlate;
}
public String getLicensePlate() {
return licensePlate;
}
}
final class Car extends Vehicle {
private final int numberOfDoors;
public Car(String licensePlate, int numberOfDoors) {
super(licensePlate);
this.numberOfDoors = numberOfDoors;
}
}
final class Truck extends Vehicle {
private final double cargoCapacity;
public Truck(String licensePlate, double cargoCapacity) {
super(licensePlate);
this.cargoCapacity = cargoCapacity;
}
}
final class Motorcycle extends Vehicle {
private final boolean hasSidecar;
public Motorcycle(String licensePlate, boolean hasSidecar) {
super(licensePlate);
this.hasSidecar = hasSidecar;
}
}
This design makes it clear that Vehicle has exactly three subtypes, preventing unexpected extensions.
2. Enhanced Security
By controlling the inheritance hierarchy, you prevent malicious or accidental subclassing that could compromise your application’s security or business logic.
3. Better Pattern Matching
Sealed classes work beautifully with pattern matching for switch expressions, enabling exhaustiveness checking.
public double calculateTax(Vehicle vehicle) {
return switch (vehicle) {
case Car c -> 100.0;
case Truck t -> 200.0;
case Motorcycle m -> 50.0;
// No default needed - compiler knows all cases are covered!
};
}
Permitted Subclass Modifiers
Each permitted subclass must be declared with one of three modifiers:
1. final - No Further Subclassing
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
public double area() {
return Math.PI * radius * radius;
}
}
2. sealed - Controlled Further Subclassing
public sealed class Rectangle extends Shape permits Square {
protected final double width;
protected final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() {
return width * height;
}
}
public final class Square extends Rectangle {
public Square(double side) {
super(side, side);
}
}
3. non-sealed - Reopens the Hierarchy
public non-sealed class Triangle extends Shape {
private final double base;
private final double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
public double area() {
return 0.5 * base * height;
}
}
// Now any class can extend Triangle
public class EquilateralTriangle extends Triangle {
public EquilateralTriangle(double side) {
super(side, side * Math.sqrt(3) / 2);
}
}
Real-World Example: Payment Processing
Here’s a practical example modeling different payment methods:
public sealed interface Payment permits CreditCardPayment, PayPalPayment, BankTransferPayment {
boolean process(double amount);
}
public final class CreditCardPayment implements Payment {
private final String cardNumber;
private final String cvv;
public CreditCardPayment(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}
@Override
public boolean process(double amount) {
System.out.println("Processing $" + amount + " via credit card");
// Credit card processing logic
return true;
}
}
public final class PayPalPayment implements Payment {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public boolean process(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
// PayPal processing logic
return true;
}
}
public final class BankTransferPayment implements Payment {
private final String accountNumber;
private final String routingNumber;
public BankTransferPayment(String accountNumber, String routingNumber) {
this.accountNumber = accountNumber;
this.routingNumber = routingNumber;
}
@Override
public boolean process(double amount) {
System.out.println("Processing $" + amount + " via bank transfer");
// Bank transfer processing logic
return true;
}
}
Now you can safely process payments knowing all possible payment types:
public void executePayment(Payment payment, double amount) {
String paymentType = switch (payment) {
case CreditCardPayment cc -> "Credit Card";
case PayPalPayment pp -> "PayPal";
case BankTransferPayment bt -> "Bank Transfer";
};
System.out.println("Using " + paymentType);
payment.process(amount);
}
Module and Package Constraints
Sealed classes and their permitted subclasses must be in the same module. If they’re in an unnamed module, they must also be in the same package. This ensures that the sealed hierarchy is well-defined and accessible.
Reflection Support
Java 17 added reflection support for sealed classes:
Class<Shape> shapeClass = Shape.class;
if (shapeClass.isSealed()) {
Class<?>[] permittedSubclasses = shapeClass.getPermittedSubclasses();
for (Class<?> subclass : permittedSubclasses) {
System.out.println("Permitted: " + subclass.getSimpleName());
}
}
Conclusion
Sealed Classes in Java 17 provide a robust way to model closed hierarchies, improve code maintainability, and enhance security. By explicitly controlling which classes can extend your types, you create more predictable and safer code. Combined with pattern matching, sealed classes enable the compiler to verify exhaustiveness, catching potential bugs at compile time rather than runtime.