Unveiling Sealed Classes in Java: Control, Clarity, and Beyond

Sealed classes were introduced in Java 15 to provide more precise control over inheritance hierarchies within your applications. Here’s why they’re useful:

  • Class Design: Sealed classes allow you to explicitly define which subclasses are allowed to extend them. This helps in creating robust, well-defined class structures, making them easier to reason about.
  • Pattern Matching: Sealed classes are a key ingredient in modern Java’s pattern matching features, simplifying type checking and ensuring proper handling of all possible types within a hierarchy.
  • Predictability & Maintainability: By restricting inheritance, you reduce the chance of unexpected extensions and make your code more predictable and maintainable over time.

How to Use Sealed Classes

  1. The sealed Keyword: You declare a class as sealed by adding the sealed modifier before the class declaration:

2. The permits Clause: A sealed class must have a permits clause to specify the list of subclasses that are allowed to extend it.

  • Subclasses: Permitted subclasses can be declared in the same file as the sealed class or in separate files. They can use the following keywords:
    • final – The class cannot be further extended.
    • sealed – The class becomes sealed and also gets its own permits clause.
    • non-sealed – The class becomes openly inheritable but cannot extend the sealed class any further.
public sealed class Shape permits Circle, Rectangle {
  ...
}

public final class Circle extends Shape {
  ...
}

public non-sealed class Triangle extends Shape {
  ...
}

Simplifying pattern matching

Pattern matching with switch expressions in Java 17 utilizes sealed classes effectively. You can exhaustively match against all possible subclasses of a sealed class, ensuring you handle all cases explicitly.

public sealed class File permits TextFile, ImageFile {
  public abstract String getName();
}

public class TextFile extends File {
  // ...
}

public class ImageFile extends File {
  // ...
}

public String processFile(File file) {
  return switch (file) {
    case TextFile textFile -> textFile.getName() + " content";
    case ImageFile imageFile -> imageFile.getName() + " metadata";
  };
}

Additional benefits:

  • Sealed classes can improve code readability by clearly communicating the intended inheritance hierarchy.
  • They can help prevent bugs by catching potential type-related issues at compile time.

Implementing a state machine

public sealed class OrderState permits Placed, Shipped, Delivered, Cancelled {
  public abstract String getStatusDescription();
}

public final class Placed extends OrderState {
  @Override
  public String getStatusDescription() {
    return "Order received and is being processed.";
  }
}

public final class Shipped extends OrderState {
  @Override
  public String getStatusDescription() {
    return "Order is shipped and on its way.";
  }
}

public final class Delivered extends OrderState {
  @Override
  public String getStatusDescription() {
    return "Order is delivered.";
  }
}

public final class Cancelled extends OrderState {
  @Override
  public String getStatusDescription() {
    return "Order is cancelled.";
  }
}

Here, OrderState is a sealed class representing different states an order can be in. Each state is a final subclass with its own implementation of the getStatusDescription() method. This structure ensures that only valid states are used and promotes clarity and maintainability when handling order processing logic. Sealed classes explicitly define the allowed subclasses of OrderState, preventing the creation of unexpected states in the future

You may also like...