Sealed Classes

Quick Summary:

  • Lets classes and interfaces restrict their subtypes, ensuring more predictable and maintainable code by controlling inheritance explicitly.
  • Released in Java 17 (2021), but introduced in Java 15 (2021).

Key Features

  • Controlled Extensibility: Sealed classes can define their allowed subtypes, which restricts how they can be extended or implemented, preventing unintended usage and enhancing encapsulation.
  • Comprehensive Type System: Works together with records (JEP 395) and pattern matching (JEP 394) to create a more robust and expressive type system.
  • Improved Maintainability: Helps to clearly communicate the design intention of the code and ensures that all subclasses are known and located in a controlled fashion.

Example

Before Java 17:

Without sealed classes, any class could extend another class if it wasn’t declared as final, leading to potentially unpredictable subclassing.

public abstract class Shape {
    // Class body omitted for brevity
    public abstract double calcArea();
}

public class Circle extends Shape {
    // Circle implementation
}

public class Triangle extends Shape {
    // Triangle implementation
}

public class Square extends Shape {
    // Square implementation
}

public class FooBarShape extends Shape {
    @Override
    public double calcArea() {
        return -Math.sqrt(2); // 🚩 Problematic uncontrolled subclass with potential for bugs and inconsistencies
    }
}

After Java 17:

With sealed classes you can strictly specify your hierarchy from the superclass.

public sealed abstract class Shape permits Circle, Square, Triangle {
    // Class body omitted for brevity
    public abstract double calcArea();
}

public final class Circle extends Shape {
    // Circle implementation
}

public final class Triangle extends Shape {
    // Triangle implementation
}

public final class Square extends Shape {
    // Square implementation
}

public class FooBarShape extends Shape { // ❌ 'FooBarShape' is not allowed in the sealed hierarchy
    // Problematic implementation avoided βœ…
}
  • In this setup, Shape is a sealed class that explicitly permits Circle, Triangle and Square as its direct subclasses.
  • Circle, Triangle and Square are all final classes, meaning they cannot be subclassed further.
  • FooBarShape, having not been explicitly permitted by the sealed class Shape, causes a compilation error as it’s trying to inherit a sealed class without permission.

Creating Sealed Classes

Making a sealed class involves adding some keywords to your normal class/record declaration:

  1. Declare the sealed type: Use keyword record.
  2. Declare the components: Follow the name up with parentheses wherein you declare the components/fields, similar to a constructor.
public record Product(Long id, String name, double price) {
}

πŸ‘©β€πŸ’» Hands-on Demo: Sealed Classes

πŸ”Ž Click here to expand

1. Multiple Choice Quiz

What is the primary purpose of introducing sealed classes in Java?

A) To allow classes to be sealed against modification
B) To restrict which other classes or interfaces may extend or implement them
C) To automatically seal all methods within a class
D) To integrate better with Java’s reflection APIs

2. Fill-in-the-Blanks

  • Fill in the blanks to correctly declare a sealed class and its permitted subclasses in Java.
______ sealed class Vehicle ______ Car, Truck {
}

______ class Car extends Vehicle {
}

final class Truck extends Vehicle {
}

3. Debugging Challenge: Code Correction

Below is an attempt to define a sealed class hierarchy in Java. However, the code contains several mistakes. Identify and correct them so that the code compiles successfully.

public sealed interface Animal permits Cat, Dog {
    void makeSound();
}

class Cat extends Animal {  // Error here
    public void makeSound() {
        System.out.println("Meow");
    }
}

class Dog implements Animal {  // Error here
    public void makeSound() {
        System.out.println("Bark");
    }
}

4. Vehicles

  1. Create a new abstract Vehicle-class that contains some basic fields (e.g. colour, movementSpeed, etc.) and abstract method (e.g. move()).
  2. Create 2 subclasses Car (which has an extra breakHorsePower field) and Bicycle (with a nrOfGears field), each of which provides its own implementation of move().
  3. Restrict the subtyping of Vehicle to just those two subclasses you just made.
  4. Double-check that you’ve made the subclasses final so that it can still compile.
  1. Create another subclass, Airplane, that also inherits Vehicle. What happens? Why?
  2. In a VehiclesApp-class, in the main-method, make a list of 3 vehicles (polymorphism).

Solutions

πŸ•΅οΈβ€β™‚οΈ Click here to reveal the solutions

1. Multiple Choice Quiz

Answer: B) To restrict which other classes or interfaces may extend or implement them

2. Fill-in-the-Blanks

______ sealed class Vehicle ______ Car, Truck {
}

______ class Car extends Vehicle {
}

final class Truck extends Vehicle {
}
public sealed class Vehicle permits Car, Truck {
}

final class Car extends Vehicle {
}

final class Truck extends Vehicle {
}

3. Debugging Challenge: Code Correction

Original:
public sealed interface Animal permits Cat, Dog {
    void makeSound();
}

class Cat extends Animal {  // Error here
    public void makeSound() {
        System.out.println("Meow");
    }
}

class Dog implements Animal {  // Error here
    public void makeSound() {
        System.out.println("Bark");
    }
}
Corrected:
public sealed interface Animal permits Cat, Dog {
    void makeSound();
}

final class Cat implements Animal {  // Corrected: 'implements' used, and class marked as 'final'
    public void makeSound() {
        System.out.println("Meow");
    }
}

final class Dog implements Animal {  // Corrected: class marked as 'final'
    public void makeSound() {
    System.out.println("Bark");
    }
}
Explanation:
  • The keyword implements should be used instead of extends for interfaces.
  • The Cat-class and Dog-class must be declared with final or another subclass-specific modifier to align with the sealed contract unless it is intended to be non-sealed, but then it would need to be explicitly stated.

4. Vehicles

public abstract sealed class Vehicle permits Car, Bicycle {
    private String colour;
    private double movementSpeed;

    public Vehicle(String colour) {
        this.colour = colour;
    }

    public String getColour() {
        return colour;
    }

    public void setColour(String colour) {
        this.colour = colour;
    }

    public double getMovementSpeed() {
        return movementSpeed;
    }

    public void setMovementSpeed(double movementSpeed) {
        this.movementSpeed = movementSpeed;
    }

    public abstract void move();
}
public final class Car extends Vehicle {
    private double breakHorsePower;

    public Car(String colour, double breakHorsePower) {
        super(colour);
        this.breakHorsePower = breakHorsePower;
    }

    @Override
    public void move() {
        System.out.printf("The %s car is moving at %f km/h with a break horse power of %f kW%n",
                getColour(), getMovementSpeed(), breakHorsePower);
    }
}
public final class Bicycle extends Vehicle {
    private int nrOfGears;

    public Bicycle(String colour, int nrOfGears) {
        super(colour);
        this.nrOfGears = nrOfGears;
    }

    public int getNrOfGears() {
        return nrOfGears;
    }

    public void setNrOfGears(int nrOfGears) {
        this.nrOfGears = nrOfGears;
    }

    @Override
    public void move() {
        System.out.printf("The %s bike is moving at %f km/h%n", getColour(), getMovementSpeed());
    }
}
public class Airplane extends Vehicle { // ❌ 'Airplane' is not allowed in the sealed hierarchy
    // Class body omitted for brevity
}
public class VehiclesApp {
    public static void main(String[] args) {
        List<Vehicle> vehicles = List.of(
                new Car("red", 100.0),
                new Bicycle("blue", 2),
                new Car("green", 120.0),
                new Bicycle("yellow", 4)
        );

    }
}

5. Shapes

public interface Shape {
    double calcPerimeter();
    double calcArea();
}
public record Rectangle(double width, double height) implements Shape {
    public Rectangle {
        if (width <= 0) throw new IllegalArgumentException("width is negative or zero");
        if (height <= 0) throw new IllegalArgumentException("height is negative or zero");
    }

    @Override
    public double calcPerimeter() {
        return 2 * (width + height);
    }

    @Override
    public double calcArea() {
        return width * height;
    }
}
public record Circle(double radius) implements Shape {
    public Circle {
        if (radius <= 0) throw new IllegalArgumentException("radius is negative or zero");
    }

    @Override
    public double calcPerimeter() {
        return 2 * Math.PI * radius;
    }

    @Override
    public double calcArea() {
        return Math.PI * (radius * radius);
    }
}
public class ShapesApp {
    public static void main(String[] args) {
        List<Shape> shapes = List.of(
                new Circle(5),
                new Rectangle(3, 4),
                new Rectangle(4, 3)
        );

        for (Shape s : shapes) {
            System.out.println(s);
            System.out.printf("\tPerimeter: %.2f\n", s.calcPerimeter());
            System.out.printf("\tArea: %.2f\n", s.calcArea());
        }
    }
}

The non-sealed Keyword

  • The introduction of sealed classes in Java also brought about the non-sealed keyword.
  • This keyword allows classes within a sealed hierarchy to opt-out of the restrictions imposed by sealing.
  • It gives developers the control to define an inheritance chain that is partially closed and partially open, offering a balance between strict encapsulation and flexibility.

Example

public sealed class Shape permits Circle, Rectangle {
    // Common methods for shapes
}

final class Circle extends Shape {
    // Circle-specific methods; no further subclassing allowed
}

non-sealed class Rectangle extends Shape {
    // Rectangle-specific methods; subclassing is allowed
}

// This subclassing is permissible because Rectangle is non-sealed
class Square extends Rectangle {
    // Further customization specific to squares
}

πŸ‘©β€πŸ’» Hands-on Demo: The non-sealed Keyword

πŸ”Ž Click here to expand

1. Vehicles (part II)

  1. Adapt your Car-class so that it is no longer final but still part of the sealed hierarchy.
  2. Create 2 new subclasses for Car, the ElectricCar-class and the CombustibleEngineCar-class.
  3. Update your list of vehicles in VehiclesApp to include objects of your 2 new subclasses.

Solutions

πŸ•΅οΈβ€β™‚οΈ Click here to reveal the solutions

1. Vehicles (part II)

public non-sealed class Car extends Vehicle {
    private double breakHorsePower;

    public Car(String colour, double breakHorsePower) {
        super(colour);
        this.breakHorsePower = breakHorsePower;
    }

    @Override
    public void move() {
        System.out.printf("The %s car is moving at %f km/h with a break horse power of %f kW%n",
                getColour(), getMovementSpeed(), breakHorsePower);
    }
}
public class ElectricCar extends Car {
    public ElectricCar(String colour, double breakHorsePower) {
        super(colour, breakHorsePower);
    }
}
public class CombustibleEngineCar extends Car {
    public CombustibleEngineCar(String colour, double breakHorsePower) {
        super(colour, breakHorsePower);
    }
}
public class VehiclesApp {
    public static void main(String[] args) {
        List<Vehicle> vehicles = List.of(
                new Car("red", 100.0),
                new Bicycle("blue", 2),
                new Car("green", 120.0),
                new Bicycle("yellow", 4),
                new ElectricCar("purple", 150.0),
                new CombustibleEngineCar("orange", 200.0)
        );

    }
}

Reality Check: Practical Uses of Sealed Classes

Below are some of the most common use cases where sealed classes prove particularly beneficial:

1. Domain Modeling

Sealed classes are excellent for domain-driven design where you need to represent a fixed set of closely related types. They help enforce business rules at the compiler level, ensuring that the domain model remains consistent and valid throughout its lifecycle.

  • Example: In a financial application, you might have a sealed class Transaction with specific subclasses like Deposit, Withdrawal, and Transfer. This setup ensures that all transactions processed in the system are one of these defined types, and no unexpected transaction types can exist.
πŸ”Ž Click to view code example
public sealed class Transaction permits Deposit, Withdrawal, Transfer {
    // Common transactional logic here
}

public final class Deposit extends Transaction {
    // Specific logic for a deposit
}

public final class Withdrawal extends Transaction {
    // Specific logic for a withdrawal
}

public non-sealed class Transfer extends Transaction {
    // Specific logic for a transfer, allows further subclassing if needed
}

2. Defining Finite State Machines

In state machine implementations, sealed classes can define a finite number of states and transitions, which helps in managing state transitions explicitly and safely.

  • Example: Consider a workflow system where a document can be in states such as Draft, Review, or Published. Using a sealed class to define these states can ensure that transitions between states are handled cleanly and that all possible states are explicitly known and handled.
πŸ”Ž Click to view code example
public sealed class DocumentState permits Draft, Review, Published {
    // Base methods common to all states
}

public final class Draft extends DocumentotState {
    // Methods specific to the draft state
}

public final class Review extends DocumentState {
    // Methods specific to the review state
}

public final class Published extends DocumentState {
    // Methods specific to the published state
}

3. ⚠ Spoiler Alert for JDK 21: Compiler-Assisted Pattern Matching

Starting from Java SE 21, sealed classes are particularly useful with pattern matching, as they allow the compiler to guarantee that all possible subtypes are covered in a switch expression or statement, eliminating the need for a default case. This leads to safer code that is less prone to errors.

  • Example: When handling various shapes in a graphic editor, a sealed class Shape could have subclasses such as Circle, Square, and Rectangle. Using pattern matching, you can easily and safely compute properties like area or perimeter, with compile-time checking to ensure all types are handled.
πŸ”Ž Click to view code example
    public static void printShapeDetails(Shape shape) {
        double area = shape.area();
        double perimeter = shape.perimeter();

        String details = switch (shape) {
            case Circle c -> String.format("Circle with radius %.2f", c.radius);
            case Rectangle r -> String.format("Rectangle with length %.2f and width %.2f", r.length, r.width);
            case Triangle t -> String.format("Triangle with sides %.2f, %.2f, %.2f", t.a, t.b, t.c);
            default -> throw new IllegalStateException("Unexpected shape type: " + shape.getClass());
        };

        System.out.printf("%s has an area of %.2f and a perimeter of %.2f%n", details, area, perimeter);
    }

4. API Design

When designing APIs, especially libraries or frameworks, sealed classes can be used to restrict how developers interact with the API, guiding them towards correct usage and preventing misuse by limiting subclassing to a known set of classes.

  • Example: In a plugin architecture, you might define a sealed class Plugin that all plugins must extend. Specific types of plugins can be defined as final classes extending Plugin, ensuring all plugins adhere to a specific contract while preventing further subclassing.
πŸ”Ž Click to view code example
public sealed class Plugin permits AudioPlugin, VideoPlugin {
    // Common plugin functionality
}

public final class AudioPlugin extends Plugin {
    // Audio-specific plugin functionality
}

public final class VideoPlugin extends Plugin {
    // Video-specific plugin functionality
}

5. Ensuring Security and Consistency

Sealed classes enhance security by controlling subclassing, which is crucial in environments where the integrity and consistency of data or behavior are paramount. This can prevent malicious subclassing or inadvertent errors in extending classes in security-sensitive applications.

  • Example: In a security framework handling different types of authentication mechanisms, sealed classes can ensure that only allowed authentication types are implemented and handled, preventing the addition of unverified or insecure implementations.
πŸ”Ž Click to view code example
public sealed class AuthenticationType permits PasswordAuth, TokenAuth {
    // Common authentication functionality
}

public final class PasswordAuth extends AuthenticationType {
    // Password authentication logic
}

public final class TokenAuth extends AuthenticationType {
    // Token authentication logic
}