4 minutes
Controlling Inheritance with Java’s Sealed Classes and Interfaces
Introduced as a standard feature in Java 17, sealed classes and sealed interfaces give you precise control over your class hierarchies. By declaring a class or interface as sealed, you can restrict which other classes or interfaces are allowed to extend or implement it.
This feature is a powerful tool for domain modeling, enhancing security, and working with pattern matching in switch expressions.
What are Sealed Classes?
A sealed class restricts which other classes can inherit from it. This provides better control over the class hierarchy, making your code more robust and maintainable.
Syntax:
public sealed class Vehicle permits Car, Truck {
// class body
}
In this example, Vehicle is a sealed class, and only Car and Truck are permitted to extend it.
Rules for Subclasses
Any class that extends a sealed class must:
- Be listed in the
permitsclause of the sealed class. - Be in the same module or package as the sealed class.
- Explicitly declare itself as either:
final: Cannot be extended further.sealed: Can be extended, but only by the classes it permits.non-sealed: Removes the sealing restriction, allowing any class to extend it.
Example Hierarchy:

// Car is final and cannot be extended
public final class Car extends Vehicle {
// ...
}
// Truck is non-sealed, so any class can extend it
public non-sealed class Truck extends Vehicle {
// ...
}
What are Sealed Interfaces?
Similarly, a sealed interface restricts which classes or interfaces can implement or extend it.
Syntax:
public sealed interface Shape permits Circle, Rectangle, Square {
// interface body
}
Here, only Circle, Rectangle, and Square are allowed to implement the Shape interface.
Rules for Implementers
The same rules for subclasses of sealed classes apply to classes that implement a sealed interface. They must be final, sealed, or non-sealed.
Example Hierarchy:

public final class Circle implements Shape { /* ... */ }
public final class Rectangle implements Shape { /* ... */ }
// A record is implicitly final
public record Square(int side) implements Shape { /* ... */ }
Sealed Types and Pattern Matching
One of the biggest benefits of sealed types is how they enhance pattern matching in switch expressions. When you switch over a sealed type, the compiler knows all the permitted subtypes. This allows it to perform an exhaustive check, ensuring that you have handled all possible cases.
If you cover all the permitted subtypes, you don’t need a default clause, which makes your code safer and more readable.
Example with an Enhanced switch
sealed interface Vehicle permits Car, Truck, Bike {}
final class Car implements Vehicle {
public int getNumberOfSeats() { return 4; }
}
final class Truck implements Vehicle {
public int getLoadCapacity() { return 5000; }
}
record Bike(String type) implements Vehicle {}
public class VehicleProcessor {
public static String processVehicle(Vehicle vehicle) {
// The switch is exhaustive because Vehicle is sealed.
// No default case is needed.
return switch (vehicle) {
case Car c -> "Processing a car with " + c.getNumberOfSeats() + " seats.";
case Truck t -> "Processing a truck with a load capacity of " + t.getLoadCapacity() + " kg.";
case Bike b -> "Processing a " + b.type() + " bike.";
};
}
public static void main(String[] args) {
System.out.println(processVehicle(new Car()));
System.out.println(processVehicle(new Truck()));
System.out.println(processVehicle(new Bike("Mountain")));
}
}
If a new subtype were added to the Vehicle interface’s permits list, the compiler would flag the switch expression as an error until it is updated to handle the new type.
Key Benefits of Sealed Types
- Controlled Inheritance: Prevents unintended extensions, making your class hierarchy predictable.
- Enhanced Security: Restricts who can implement sensitive APIs.
- Improved Readability: Clearly defines the intended inheritance structure.
- Exhaustive Compiler Checks: Enables exhaustive checks in
switchexpressions, eliminating the need for adefaultcase and reducing bugs. - Precise Domain Modeling: Helps create more accurate and explicit domain models.
- Better Maintainability: Makes code easier to understand, refactor, and evolve.