Java sealed classes, introduced in Java 15 as a preview feature and finalized in Java 17, allow developers to define which classes or interfaces can extend or implement a given class or interface.
This provides more control over class hierarchies and helps achieve more predictable and maintainable code.
Key Features of Sealed Classes
- Explicit Control: Sealed classes explicitly control which classes are allowed to extend them.
- Enhanced Security and Maintainability: By restricting subclasses, you can ensure a limited set of extensions, making the code easier to reason about.
- Compile-Time Safety: The compiler can check that only the specified subclasses extend or implement the sealed class or interface.
Syntax Overview
A sealed class is defined using the sealed keyword, followed by a permits clause that lists the classes allowed to extend it. Any class that extends a sealed class must use one of the following modifiers:
- final: Indicates that this subclass cannot be extended further.
- sealed: Indicates that the subclass itself is also sealed, continuing the restriction to further subclasses.
- non-sealed: Removes the sealing constraint for this subclass, allowing it to be extended freely.
public sealed class Parent permits Child1, Child2 { // Class contents } public final class Child1 extends Parent { // Class contents } public sealed class Child2 extends Parent permits GrandChild { // Class contents } public non-sealed class GrandChild extends Child2 { // Class contents }
Example 1: Basic Sealed Class
In this example, we define a sealed class Shape that has three permitted subclasses: Circle, Rectangle, and Square.
public sealed class Shape permits Circle, Rectangle, Square { public abstract double area(); } public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } public final class Rectangle extends Shape { private final double width; private final double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } public final class Square extends Shape { private final double side; public Square(double side) { this.side = side; } @Override public double area() { return side * side; } }
Explanation
- Shape is a sealed class that permits only Circle, Rectangle, and Square as subclasses.
- Each subclass overrides the area method to calculate the area of the respective shape.
- Each subclass is marked as final, meaning no further subclassing is allowed.
Example 2: Sealed Class with non-sealed Subclass
In this example, we modify the hierarchy by allowing the Rectangle class to be further extended.
public sealed class Shape permits Circle, Rectangle, Square { public abstract double area(); } public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } public non-sealed class Rectangle extends Shape { private final double width; private final double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } public final class Square extends Shape { private final double side; public Square(double side) { this.side = side; } @Override public double area() { return side * side; } } // A class extending Rectangle since it is non-sealed public class ColoredRectangle extends Rectangle { private String color; public ColoredRectangle(double width, double height, String color) { super(width, height); this.color = color; } public String getColor() { return color; } }
Explanation
- Rectangle is marked as non-sealed, allowing it to be further extended.
- ColoredRectangle extends Rectangle and adds a new property color.
- Shape is still restricted to three subclasses, but Rectangle can have further subclasses due to the non-sealed modifier.
Example 3: Sealed Interface
Sealed classes are not limited to classes; they can also be applied to interfaces. Here’s an example with a sealed interface Vehicle.
public sealed interface Vehicle permits Car, Bike, Truck { void drive(); } public final class Car implements Vehicle { @Override public void drive() { System.out.println("Driving a car."); } } public final class Bike implements Vehicle { @Override public void drive() { System.out.println("Riding a bike."); } } public non-sealed class Truck implements Vehicle { @Override public void drive() { System.out.println("Driving a truck."); } } public class PickupTruck extends Truck { @Override public void drive() { System.out.println("Driving a pickup truck."); } }
Explanation
- Vehicle is a sealed interface, permitting only Car, Bike, and Truck.
- Car and Bike are final classes, while Truck is non-sealed, allowing it to be further extended by other classes, such as PickupTruck.
Example 4: Sealed Class with Nested Classes
Using sealed classes with nested classes can help in encapsulating logic within a single outer class.
public class Company { public sealed class Employee permits Developer, Manager, Tester { public String name; public Employee(String name) { this.name = name; } public String getName() { return name; } } public final class Developer extends Employee { public Developer(String name) { super(name); } public void develop() { System.out.println(name + " is developing code."); } } public final class Manager extends Employee { public Manager(String name) { super(name); } public void manage() { System.out.println(name + " is managing the team."); } } public final class Tester extends Employee { public Tester(String name) { super(name); } public void test() { System.out.println(name + " is testing the code."); } } }
Explanation
- Employee is a sealed nested class within Company, permitting only Developer, Manager, and Tester as subclasses.
- This allows the Company class to maintain strict control over the types of Employee objects that can exist within it.
- Each subclass (e.g., Developer, Manager, and Tester) is marked as final.
Example 5: Using Sealed Classes in Pattern Matching (Java 17+)
With sealed classes, pattern matching in switch statements becomes more robust since the compiler knows all possible subclasses. This is especially useful when working with sealed hierarchies in switch expressions.
public sealed class Shape permits Circle, Rectangle, Square { public abstract double area(); } public final class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } public final class Rectangle extends Shape { private final double width; private final double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } public final class Square extends Shape { private final double side; public Square(double side) { this.side = side; } @Override public double area() { return side * side; } } // Pattern matching with a sealed class public class TestPatternMatching { public static void printShapeDetails(Shape shape) { switch (shape) { case Circle c -> System.out.println("Circle with area: " + c.area()); case Rectangle r -> System.out.println("Rectangle with area: " + r.area()); case Square s -> System.out.println("Square with area: " + s.area()); } } public static void main(String[] args) { Shape shape = new Circle(5.0); printShapeDetails(shape); shape = new Rectangle(4.0, 6.0); printShapeDetails(shape); } }
Explanation
- The Shape class hierarchy allows using pattern matching with switch.
- The switch statement in printShapeDetails uses shape to match against Circle, Rectangle, and Square.
- This pattern matching is exhaustive, meaning all cases are covered. If new subclasses are added to Shape, the compiler will enforce covering them in the switch, ensuring compile-time safety.
Summary
Java sealed classes provide a powerful way to control inheritance hierarchies, making it easier to enforce restricted extensions and ensuring safer, more maintainable code. Key points include:
- Sealed classes restrict which classes can extend or implement them using the permits clause.
- Modifiers for subclasses:
- final to prevent further subclassing.
- sealed to continue the hierarchy restrictions.
- non-sealed to remove the restriction, allowing further subclassing.
- Pattern Matching Support in Java 17 and later, allowing exhaustive case matching.
Sealed classes are helpful in domain-driven design, data modeling, and any situation where a limited and predictable set of types is beneficial.