Sealed Classes in Java: A Modern Approach to OOP

Object-oriented programming (OOP) has been the backbone of Java development for decades. Over time, Java has introduced several features that refine how we model real-world concepts into software. One of the most exciting modern additions is sealed classes. When I first started exploring sealed classes, I found them to be a powerful tool for better control over class hierarchies and a cleaner way to express intent in my designs.

In this article, I want to share what sealed classes are, how they fit into the world of Java’s OOP, why they matter, and how I have applied them to write safer, more maintainable code.

What Are Sealed Classes?

At its core, a sealed class is a class or interface that restricts which other classes or interfaces can extend or implement it. Unlike traditional classes or interfaces, which anyone can subclass or implement, sealed classes allow the author to declare a fixed set of permitted subclasses.

This control over subclassing enables more robust APIs and clearer designs because the class hierarchy becomes explicit and tightly controlled.

Why Did Java Introduce Sealed Classes?

Before sealed classes, controlling who could extend a class was limited to access modifiers (public, protected, package-private, private), but these were blunt instruments. For example, if a class is public, anyone can extend it. If it is final, no one can extend it at all. There was no in-between.

Sometimes, I wanted to allow only specific subclasses that I trusted or designed while preventing any other unforeseen extensions. Sealed classes fill this gap perfectly by giving explicit control over inheritance.

This feature aligns well with principles like encapsulation and domain-driven design, where controlling the model is critical.

How Sealed Classes Work

To declare a sealed class, you use the sealed modifier in the class or interface declaration. Along with that, you specify the permitted subclasses using the permits clause.

Here’s a simple example I experimented with:

java public sealed class Vehicle permits Car, Truck {
    // Common vehicle behavior
}

public final class Car extends Vehicle {
    // Car-specific implementation
}

public non-sealed class Truck extends Vehicle {
    // Truck-specific implementation
}

In this example:

  • Vehicle is sealed and explicitly permits only Car and Truck to extend it.
  • Car is declared final, meaning it cannot be subclassed.
  • Truck is declared non-sealed, allowing it to be subclassed further.

This structure clearly communicates intended extensions and enforces them at compile time.

Key Rules About Sealed Classes

From my experiments, I found some important rules about sealed classes:

  1. Permitted subclasses must be in the same module or package (depending on module system usage).
  2. Every permitted subclass must declare itself as either final, sealed, or non-sealed. This ensures the sealing contract continues through the hierarchy.
  3. Non-sealed subclasses effectively break the sealing contract, allowing further extension.
  4. Permits clause must explicitly list all permitted subclasses; omission results in compile-time errors.

Benefits of Using Sealed Classes

Better Control Over Inheritance

The most obvious advantage is precise control. I can prevent misuse by other developers or unintended subclassing, reducing bugs caused by unforeseen extensions.

Improved Exhaustiveness Checking

Sealed classes work hand-in-hand with pattern matching (introduced in recent Java versions). When using switch statements or enhanced instanceof checks, the compiler knows all possible subclasses and can warn if cases are not handled.

This reduces runtime errors and makes my code safer and easier to maintain.

Cleaner and More Expressive APIs

Sealed classes express the author’s design intent more clearly than just comments or documentation. When I see a sealed class, I immediately understand the limited extension points.

Better Security and Encapsulation

Restricting subclasses means less surface area for vulnerabilities or misuse, which is especially valuable in public APIs and libraries.

Practical Examples

Modeling a Payment System

Consider a payment processing system. I want to represent payment methods but restrict them to known types.

java public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer {}

public final class CreditCard implements PaymentMethod {
    // Credit card details
}

public final class PayPal implements PaymentMethod {
    // PayPal account info
}

public non-sealed class BankTransfer implements PaymentMethod {
    // Bank transfer details
}

In this case, I permit only these three classes to implement the PaymentMethod interface. However, BankTransfer is marked non-sealed, allowing extensions if future requirements arise.

This design lets me handle all payment methods exhaustively in switch expressions without worrying about unknown implementations.

Handling Shapes

Another common example is geometry:

java public sealed abstract class Shape permits Circle, Rectangle, Square {}

public final class Circle extends Shape {
    private double radius;
    // Circle-specific code
}

public sealed class Rectangle extends Shape permits FilledRectangle, EmptyRectangle {}

public final class FilledRectangle extends Rectangle {
    // Filled rectangle
}

public final class EmptyRectangle extends Rectangle {
    // Empty rectangle
}

public final class Square extends Shape {
    private double side;
    // Square-specific code
}

Here, I created a hierarchy with a sealed base Shape. Rectangle is sealed again, permitting only two subtypes. This layered sealing enforces strict control and expresses the model clearly.

Sealed Classes and Pattern Matching

Recently, Java introduced powerful pattern matching capabilities for instanceof and switch. When combined with sealed classes, pattern matching becomes safer and more expressive.

For example:

java void printShapeInfo(Shape shape) {
    switch (shape) {
        case Circle c -> System.out.println("Circle with radius " + c.getRadius());
        case FilledRectangle fr -> System.out.println("Filled rectangle");
        case EmptyRectangle er -> System.out.println("Empty rectangle");
        case Square s -> System.out.println("Square with side " + s.getSide());
    }
}

Because Shape is sealed, the compiler knows all possible subclasses and can issue warnings if any cases are missing. This eliminates the need for a default case, making the code clearer.

When Not to Use Sealed Classes

Sealed classes are powerful but not always appropriate.

  • If your design requires open extensibility where anyone can extend your classes, sealed classes are too restrictive.
  • If you work in ecosystems where modularity or packaging constraints make sealing difficult, it might be cumbersome.
  • For very small or internal classes where control isn’t critical, sealing may add unnecessary complexity.

I find that sealed classes work best in domain models where the set of subclasses is known and finite or where API safety is a priority.

Interoperability and Tooling

Sealed classes are part of the Java language starting with Java 15 as a preview and standardized in later versions. While the language supports them, some build tools, IDEs, or frameworks might lag behind in support.

I had to ensure my tools like IntelliJ IDEA and Maven were updated to handle sealed classes properly, especially for compiling and debugging.

Also, some serialization frameworks require adjustments to handle sealed classes gracefully.

How Sealed Classes Improve Code Maintenance

In one project, I used sealed classes to model various event types in an event-driven system. Before sealing, I often found rogue event implementations sneaking in, causing runtime errors and forcing exhaustive checks.

After switching to sealed classes, all event types were explicit, and the compiler helped catch missing cases. Maintenance became easier, and onboarding new team members was simpler since the hierarchy was clear and constrained.

Summary of Best Practices

  • Use sealed classes to enforce strict control over inheritance in your domain models.
  • Combine sealed classes with pattern matching for safer and cleaner code.
  • Explicitly declare permitted subclasses and decide on final, sealed, or non-sealed status based on design needs.
  • Be mindful of packaging and module boundaries when using permits.
  • Update tooling and libraries to ensure compatibility.
  • Avoid overusing sealing; use it where it brings clarity and safety.

Final Thoughts

Sealed classes bring a modern, elegant approach to object-oriented programming in Java. They help me express intent clearly, reduce bugs related to improper subclassing, and leverage new language features like pattern matching to write safer and more maintainable code.

While they require a mindset shift and some care around project structure, the benefits are well worth it. If you haven’t explored sealed classes yet, I encourage you to experiment with them in your next Java project. You might be surprised how much cleaner and more robust your class hierarchies become.

Similar Posts