Java Sealed Classes are a powerful feature and a subpar design

When I was a youngling learning to code Delphi on the hard knock streets of Johannesburg, one thing was made very clear by the compiler and teachers: circular dependencies are wrong—just wrong. No code would compile. I appreciate that we’re about 800 years past that era and tech has evolved, so it can now handle some circular dependencies. But to me, it still feels like a bad design decision.

Which brings me to Java’s permits keyword (also known as Sealed Classes and Sealed Interfaces—not TypeScript). This feature allows you to create an interface and "assign" classes to it, essentially mimicking a Union type like TypeScript; example:

sealed interface Animal permits Dog, Cat {}

Now I can use Animal in my code and perform a type check—if it’s a Dog or Cat, things just work. This also means the shapes of Dog and Cat don’t need to match (again, a Union type)... coolness.

Here’s the wild part for me: all the classes "joined" in this interface must also extend it. So Dog is an Animal, Cat is an Animal, and Animal is both Cat and Dog. Example:

final class Dog extends Animal {
    public String bark() { ... }
}

or

final class Cat extends Animal {
    public String meow() { ... }
}

This means Animal needs to know what Cat and Dog are first to set itself up, but those classes need to know Animal to use it—a circular dependency by design. It works, but it still feels bad. I likely wouldn’t have made them extend the interface if I’d had my way.

There’s a perk, though: you can force a shared shape in the interface:

sealed interface Animal permits Dog, Cat {
    boolean isTailWagging();
}

This means both classes must implement it:

final class Dog extends Animal {
    public String bark() { ... }
    public boolean isTailWagging() { ... }
}

or

final class Cat extends Animal {
    public String meow() { ... }
    public boolean isTailWagging() { ... }
}

Then we can use that without a type check:

void speak(Animal animal) {
    if (!animal.isTailWagging()) {
        // animal is unhappy so will make a noise
        switch (animal) {
            case Dog dog -> dog.bark();
            case Cat cat -> cat.meow();
        }
    }
}

Though in my fictional design, I’d just have two interfaces—one for the shared shape and one for the union...

Anyway, I hope this little tangent was interesting! Definitely good to see Java getting this kind of functionality (though it’s been around for a while). Now we just need to see people using it effectively!