ZetCode

Java Sealed Classes

last modified May 28, 2025

This article explains sealed classes in Java, a feature introduced in Java 17 to enforce controlled inheritance hierarchies. Sealed classes allow developers to limit which subclasses can extend a given class, ensuring better design enforcement and preventing unintended inheritance.

Sealed classes provide explicit control over class extension, allowing only specified subtypes to inherit from them. This approach enhances domain modeling, enforcing constraints on class hierarchies.

Key characteristics of sealed classes:

Basic Syntax

A sealed class defines a fixed set of subclasses that can extend it, as shown below. This approach ensures that only the explicitly permitted types can participate in the inheritance hierarchy, providing greater control and predictability in your codebase.

public sealed class Vehicle permits Car, Truck {
    // class members
}

Permitted subtypes must specify one of the following inheritance types:

Sealed classes provide several advantages:

Simple Sealed Class Example

Basic sealed class with implementations. This example demonstrates how to declare a sealed class and its permitted subclasses, illustrating the enforcement of controlled inheritance in practice.

Main.java
sealed abstract class Shape permits Circle, Rectangle {
    abstract double area();
}

final class Circle extends Shape {
    private final double radius;
    
    Circle(double radius) { this.radius = radius; }
    
    @Override double area() { return Math.PI * radius * radius; }
}

final class Rectangle extends Shape {
    private final double width, height;
    
    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override double area() { return width * height; }
}

void main() {

    Shape circle = new Circle(5.0);
    Shape rectangle = new Rectangle(4.0, 6.0);
    
    System.out.println("Circle area: " + circle.area());
    System.out.println("Rectangle area: " + rectangle.area());
}

In this example, we define a sealed abstract class Shape that permits only Circle and Rectangle as its subclasses. The Shape class has an abstract method area, which must be implemented by its subclasses. The Circle and Rectangle classes are marked as final, meaning they cannot be further extended. This structure ensures that the Shape hierarchy remains controlled and predictable, allowing only the specified subclasses to participate in the inheritance chain.

Sealed Class Hierarchy

Creating a hierarchy of sealed classes. This section shows how sealed classes can be extended across multiple levels, allowing for complex but well-defined inheritance structures.

Main.java
sealed class Vehicle permits Car, Truck {
    
    protected String manufacturer;
    
    Vehicle(String manufacturer) {
        this.manufacturer = manufacturer;
    }
}

sealed class Car extends Vehicle permits ElectricCar {
    Car(String manufacturer) {
        super(manufacturer);
    }
}

final class ElectricCar extends Car {
    ElectricCar(String manufacturer) {
        super(manufacturer);
    }
}

final class Truck extends Vehicle {
    Truck(String manufacturer) {
        super(manufacturer);
    }
}

void main() {

    Vehicle tesla = new ElectricCar("Tesla");
    Vehicle ford = new Truck("Ford");
    
    System.out.println(tesla.manufacturer);
    System.out.println(ford.manufacturer);
}

This example demonstrates a sealed class Vehicle with two permitted subclasses: Car and Truck. The Car class is sealed and permits only ElectricCar as a subclass. This structure allows for a controlled hierarchy where only specified subclasses can extend the Vehicle class, ensuring that the inheritance remains predictable and manageable.

Pattern Matching with Sealed Classes

Using pattern matching with sealed classes. Pattern matching with sealed classes enables concise and exhaustive handling of all possible subtypes, making your code safer and easier to maintain.

Main.java
sealed class Expr permits Constant, Add, Subtract {
    abstract int eval();
}

final class Constant extends Expr {
    private final int value;
    
    Constant(int value) { this.value = value; }
    
    @Override int eval() { return value; }
}

final class Add extends Expr {
    private final Expr left, right;
    
    Add(Expr left, Expr right) {
        this.left = left;
        this.right = right;
    }
    
    @Override int eval() { return left.eval() + right.eval(); }
}

void main() {
    Expr expr = new Add(new Constant(5), new Constant(3));
    System.out.println("Result: " + eval(expr));
}

int eval(Expr e) {
    return switch (e) {
        case Constant c -> c.eval();
        case Add a -> eval(a.left) + eval(a.right);
        case Subtract s -> eval(s.left) - eval(s.right);
    };
}

Sealed classes enable exhaustive pattern matching since all subtypes are known. This feature allows the compiler to verify that all cases are handled, reducing the risk of runtime errors due to unhandled subclasses.

Sealed Interfaces

Java also allows interfaces to be sealed, providing the same control over which classes or interfaces can implement or extend them. Sealed interfaces are useful for modeling hierarchies where both classes and interfaces need restricted extension, ensuring a fixed set of permitted implementors. This mechanism helps maintain strict boundaries in your type system and supports robust domain modeling.

Main.java
sealed interface Shape permits Circle, Rectangle, Polygon {
    double area();
}

final class Circle implements Shape {

    private final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    public double area() {
        return Math.PI * radius * radius;
    }
}

final class Rectangle implements Shape {

    private final double width, height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double area() {
        return width * height;
    }
}

non-sealed class Polygon implements Shape {

    private final double area;

    Polygon(double area) {
        this.area = area;
    }

    public double area() {
        return area;
    }
}

void main() {

    Shape c = new Circle(3);
    Shape r = new Rectangle(4, 5);
    Shape p = new Polygon(12);

    System.out.println("Circle area: " + c.area());
    System.out.println("Rectangle area: " + r.area());
    System.out.println("Polygon area: " + p.area());
}

This example demonstrates a sealed interface Shape with three permitted implementors: a final Circle, a final Rectangle, and a non-sealed Polygon that can be further extended. Sealed interfaces provide the same benefits as sealed classes, including exhaustive pattern matching and controlled extension. By sealing interfaces, you can enforce architectural constraints and improve code reliability.

The non-sealed Keyword

The non-sealed keyword in Java allows a permitted subclass of a sealed class or interface to opt out of sealing, making it open for further extension. This means that while the parent restricts which classes can extend it, a non-sealed subclass can be freely extended by any other class, removing the restriction for its own hierarchy. This flexibility is useful when you want to allow future expansion in certain branches of your class hierarchy while keeping others tightly controlled.

Main.java
sealed class Animal permits Dog, Cat, WildAnimal {}

final class Dog extends Animal {}

non-sealed class Cat extends Animal {}

class PersianCat extends Cat {}
class SiameseCat extends Cat {}

non-sealed class WildAnimal extends Animal {}

void main() {

    Cat genericCat = new Cat();
    PersianCat persian = new PersianCat();
    SiameseCat siamese = new SiameseCat();
    System.out.println("Cat: " + genericCat.getClass().getSimpleName());
    System.out.println("PersianCat: " + persian.getClass().getSimpleName());
    System.out.println("SiameseCat: " + siamese.getClass().getSimpleName());
}

In this example, Animal is a sealed class that permits Dog, Cat, and WildAnimal as subclasses. Dog is final and cannot be extended further. Cat is marked non-sealed, so it can be extended by any class, such as PersianCat and SiameseCat. This demonstrates how the non-sealed keyword reopens the inheritance hierarchy for specific subclasses, allowing for greater flexibility where needed.

The WildAnimal class is also marked non-sealed, allowing it to be extended freely. This provides flexibility in the hierarchy while still maintaining control over the base class Animal. By selectively using non-sealed, you can balance strictness and extensibility in your class designs.

When to Use Sealed Classes

Appropriate use cases include scenarios where you need to model a fixed set of implementations, define domain models with known variants, restrict API implementations, or leverage pattern matching for exhaustive handling of all possible subtypes. Sealed classes are particularly beneficial in these contexts because they provide compile-time guarantees about the structure of your type hierarchies.

Benefits of using sealed classes include better domain modeling, improved API security, support for exhaustive pattern matching, and more maintainable code. By restricting inheritance, you can prevent unintended extensions and make your codebase easier to understand and maintain.

Source

JEP 409: Sealed Classes

In this article we explored Java sealed classes for controlled inheritance. They make code more secure and maintainable through restricted hierarchies.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.