How to use sealed classes for better type safety in Java 17

Java 17 introduced sealed classes as part of the enhancements to the type system. Sealed classes allow developers to explicitly control which classes can extend or implement a class or interface, thereby achieving better type safety and making it easier to design domain-specific hierarchies. Here’s a guide on how to use sealed classes effectively:


What are Sealed Classes?

Sealed classes restrict which other classes or interfaces can extend or implement them. By using sealed classes, you can:

  1. Define a closed hierarchy of types where only a fixed set of subtypes is allowed.
  2. Ensure better maintainability and readability of your type hierarchy.
  3. Provide exhaustive handling for these types with features like switch statements.

The syntax revolves around the sealed, non-sealed, and final keywords.


Declaring and Using Sealed Classes

1. Declaration

To declare a sealed class:

  • Use the sealed modifier.
  • Specify the permitted subclasses with the permits clause.
package org.kodejava.basic;

public sealed class Shape permits Circle, Rectangle, Square {
   // Common properties and methods for all shapes
}

In this example:

  • Shape is the sealed class.
  • Only Circle, Rectangle, and Square are allowed to extend Shape.

2. Permitted Subclasses

Every subclass permitted by the sealed class must opt for one of the following:

  • final: The subclass cannot be further extended.
  • non-sealed: The subclass can be extended by any other class.
  • sealed: The subclass restricts its hierarchy further with permits.

Examples:

package org.kodejava.basic;

// Final subclass (cannot have further subclasses)
public final class Circle extends Shape {
   double radius;

   public Circle(double radius) {
      this.radius = radius;
   }
}
package org.kodejava.basic;

// Sealed subclass with its own permitted subclasses
public sealed class Rectangle extends Shape permits RoundedRectangle {
   double width, height;

   public Rectangle(double width, double height) {
      this.width = width;
      this.height = height;
   }
}
package org.kodejava.basic;

// Non-sealed subclass (can have arbitrary subclasses)
public non-sealed class Square extends Shape {
   double side;

   public Square(double side) {
      this.side = side;
   }
}
package org.kodejava.basic;

// Permitted subclass of Rectangle
public final class RoundedRectangle extends Rectangle {
    double cornerRadius;

    public RoundedRectangle(double width, double height, double cornerRadius) {
        super(width, height);
        this.cornerRadius = cornerRadius;
    }
}

Benefits of Sealed Classes

  1. Closed Type Hierarchies
    Sealed classes provide an explicit way to define and restrict type hierarchies, avoiding unintended subclasses.

  2. Exhaustiveness in switch Statements
    When all subclasses of a sealed class are known, the compiler ensures exhaustiveness in switch expressions. This helps eliminate the possibility of missing a case.

    Example:

    public double calculateArea(Shape shape) {
        return switch (shape) {
            case Circle c -> Math.PI * c.radius * c.radius;
            case RoundedRectangle rr ->
                    rr.width * rr.height - (4 - Math.PI) * rr.cornerRadius * rr.cornerRadius / 4;
            case Rectangle r -> r.width * r.height;
            case Square s -> s.side * s.side;
            default -> throw new IllegalStateException("Unexpected value: " + shape);
        };
    }
    

    If you later add a new subclass to Shape, the compiler will generate an error until you update the switch statement accordingly.

  3. Immutability and Security
    By marking direct subclasses as final or controlling further inheritance (e.g., via sealed vs. non-sealed), you ensure immutability in specific contexts and prevent unintended behavior caused by subclassing.


Practical Use Cases for Sealed Classes

  1. Domain Modelling
    Example: A sealed class Payment can have subclasses for CreditCardPayment, BankTransfer, and CryptoPayment.

    public sealed class Payment permits CreditCardPayment, BankTransfer, CryptoPayment {
       // Common payment attributes
    }
    
    public final class CreditCardPayment extends Payment {
       // Credit card specific fields
    }
    
    public final class BankTransfer extends Payment {
       // Bank transfer specific fields
    }
    
    public final class CryptoPayment extends Payment {
       // Crypto payment specific fields
    }
    
  2. Compiler Assistance for Type-Safe Code
    The sealed hierarchy ensures that when you process these types (e.g., with switch or polymorphic methods), the compiler helps enforce exhaustive handling.


Key Points to Remember

  • You must list all permitted subclasses explicitly using the permits clause.
  • All subclasses of a sealed class must be declared in the same module or package as the sealed class (enhanced encapsulation).
  • sealed, non-sealed, and final define the inheritance relation for permitted subclasses.

Summary

Sealed classes are a powerful tool in Java 17 for creating controlled and predictable type hierarchies. They help enforce constraints at compile-time, reduce runtime errors, and assist developers in creating clean, maintainable, and type-safe code. Use them effectively to create robust domain models and application logic.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.