Skip to Content
Course content

109: Sealed Classes and Interfaces

Click on the "Edit" button in the top corner of the screen to edit your slide content.

For a long time in Java, we had a binary choice when designing class hierarchies: either you made a class final, meaning nobody could ever extend it, or you left it open, meaning any class in any package could potentially subclass it. This is often too blunt an instrument. Sometimes you want to say, "I'm okay with these three specific classes extending my base, but I don't want some random developer in a different module adding a fourth one that breaks my business logic."

Restricting the Payment Hierarchy

Let's build a small payment processing system. In this scenario, I only support three payment methods: Credit Card, PayPal, and Bank Transfer. If I use a standard interface, anyone can implement PaymentMethod. But if I'm using a switch expression to handle the logic for each type, an unexpected new implementation would cause a runtime error or force me to rely on a generic default case that might hide bugs.

Here is how I'll start. I'll define a sealed interface. By using the sealed modifier and the permits clause, I'm explicitly telling the Java compiler exactly who is allowed in the club.

public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer {
    double amount();
}

Defining our Permitted Methods

Now I need to create the classes I just promised the interface. There's a catch here that often trips people up: every class that extends a sealed class must explicitly declare its own "seal status." It has to be either final (no more subclasses), sealed (further restricted subclasses), or non-sealed (opening it back up to the world).

Since these payment methods are pretty concrete, I'll make them all final.

public final class CreditCard implements PaymentMethod {
    private final double amount;
    public CreditCard(double amount) { this.amount = amount; }
    public double amount() { return amount; }
}

public final class PayPal implements PaymentMethod {
    private final double amount;
    public PayPal(double amount) { this.amount = amount; }
    public double amount() { return amount; }
}

public final class BankTransfer implements PaymentMethod {
    private final double amount;
    public BankTransfer(double amount) { this.amount = amount; }
    public double amount() { return amount; }
}

Hitting the Compiler's Wall

I'll be honest—when I first started using sealed classes, I forgot about the modifier requirement on the subclasses. I tried to write the CreditCard class as a plain public class CreditCard implements PaymentMethod. The compiler immediately yelled at me, complaining that the class must be declared final, sealed, or non-sealed.

It felt like extra boilerplate at first, but then I realized why it's there. If the compiler didn't force me to choose, the "seal" would be meaningless. If CreditCard was just a regular class, someone could create a SpecialCreditCard that extends CreditCard, and suddenly my sealed hierarchy has a leak. Java forces you to be explicit about whether the chain of restriction ends or continues.

The Payoff: Exhaustive Switching

The real magic happens when you use these classes in a switch. Because the compiler knows exactly which classes implement PaymentMethod, it can check if you've covered all possible cases. If I miss one, the code won't even compile.

Check out how clean this looks compared to a chain of instanceof checks:

public class PaymentProcessor {
    public String process(PaymentMethod method) {
        return switch (method) {
            case CreditCard cc -> "Processing credit card payment of $" + cc.amount();
            case PayPal pp -> "Redirecting to PayPal for $" + pp.amount();
            case BankTransfer bt -> "Scheduling bank transfer of $" + bt.amount();
            // No default case needed! The compiler knows these are the only three.
        };
    }
}

If I were to add a CryptoPayment class to the permits list later, the process method would immediately throw a compile-time error. This is a huge safety win; the compiler effectively becomes my checklist, ensuring I don't forget to update my business logic when the data model changes.




📋 Practical Task

Implement a Sealed Notification System

You are building a notification dispatcher. You need to ensure that only three types of notifications can exist in the system: Email, SMS, and PushNotification. No other notification types should be possible.

  • Create a sealed interface named Notification that permits Email, SMS, and PushNotification.
  • Each notification type should be a final class containing a String message.
  • Write a class called NotificationService with a method send(Notification n).
  • Inside the send method, use a switch expression to return a unique string for each notification type (e.g., "Sending Email: [message]").
  • Ensure that you do not use a default case in your switch expression, relying instead on the exhaustiveness of the sealed interface.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.