February 5, 2024

lealceldeiro

What are sealed classes in Java?

In a previous article we learned what's a Java class. From a code perspective, sealed classes are not far from a regular class, but they're somehow special in the way they can be extended. Let's see it in details.

The sealed keyword

In the aforementioned post there's a reference to a class modifier called sealed. This is the differentiator, from a code perspective, of a sealed class from a regular class.

A class is sealed when this keyword is used in its declaration. More formally, it's a class of which all direct subclasses are known when it's declared, and no other direct subclasses are desired or required ( §8.1.1.2 ). So, its declaration is the same we saw already, with the specificity that sealed must be present as a class modifier:


{ClassModifier} class TypeIdentifier [TypeParameters] [ClassExtends] [ClassImplements] [ClassPermits] ClassBody

Let's see an example:


sealed class PrimaryColor {
}

Use cases for sealed classes

Sealed classes, being a relatively new addition to Java, have enabled developers to use some features and paradigms more easily. For example, they, together with records , and pattern matching , have made much easier to write our code following a data-oriented programming approach.

I would say this is one of the cases where they fit the best: they allow us to represent in our code the real-world data structures more accurately in scenarios where inheritance is needed, but limited to only just some known subtypes, and where, therefore, "traditional" inheritance is not a perfect fit.

This means with sealed classes we can create closed type hierarchies, limited set of implementations, state machines, limited set of exceptions, and more, everything revolving around the main capability of limiting the number of subtypes that can be defined for a given class or interface.

This explicit and exhaustive control over the direct subtypes is useful when the class hierarchy is used to model the types of values in a domain, rather than as a mechanism for code inheritance and reuse.

Example

Let's consider the previous class, PrimaryColor; it represents a primary color (concept from our real-world business domain). For the sake of simplicity, let's assume red, yellow, and blue are the only possible primary colors in our business domain 1.

This naturally leads us to want to have a superclass PrimaryColor and only three subclases: Red, Yellow, and Blue. With this setup we have the data structure that represents our data, modeled as Java classes/objects. Before sealed classes came along, the code would have looked like this:


class PrimaryColor {
}

class Red extends PrimaryColor {
}

class Yellow extends PrimaryColor {
}

class Blue extends PrimaryColor {
}

Then other developers could use these classes to interact with our API. Maybe they need to send a piece of data, packed as one of these three classes. Something like this:


PrimaryColor color = new Red();
api.send(color);    // details of the `api` object are omitted here for brevity

That's what we were used to. It represents perfectly our business domain — well, almost. There's one design flaw: we're assuming only red, yellow, and blue could be sent as possible values through our api object, but that's not entirely true.

If other developers decide to extend our PrimaryColor class, or any of its subtypes, with a class of their own, they could send any other color, for example Pink which is not primary. They could even send an object apple, which is not a color, but a fruit!

So, how do we guarantee that only the specific classes we supply as possible values for primary colors are used? The answer lies in sealed classes. To be fair, there could be other options such as using the instanceof operator, but they're not as clean as using a sealed class, that's why I won't even show the code with that alternative.

Back to sealed classes, let's remodel, our code to use this feature:


sealed class PrimaryColor {
}

final class Red extends PrimaryColor {
}

final class Yellow extends PrimaryColor {
}

final class Blue extends PrimaryColor {
}

Notice how PrimaryColor was declared sealed and its subclasses were declared final. This is an improvement. Red, Yellow, and Blue cannot be further extended; and we're showing intent: anyone reading the code will realize we're trying to limit the possible subclasses our PrimaryColor can have.

Sadly, good intentions are not enough. Despite declaring a class as sealed and the subclasses final, other classes out of our control could still extend the superclass and be provided as a valid PrimaryColor value. For example, this is valid:


// somewhere, in an external code
final class Pink extends PrimaryColor {
}

To fix this, we need to use another keyword in combination with sealed.

The permits keyword

The permits clause specifies all the classes intended as direct subclasses of the class being declared (§8.1.6) and its structure is as follows:


permits TypeName {, TypeName}

This means that any other class that is declared to extend our superclass and which is not present in the superclass' permit clause will receive a compile-time error.

Let's revise our previous code to specify the permitted classes:


sealed class PrimaryColor permits Blue, Red, Yellow {
}

final class Red extends PrimaryColor {
}

final class Yellow extends PrimaryColor {
}

final class Blue extends PrimaryColor {
}

By doing this there's no way anyone can extend PrimaryColor further (without us allowing it first in the superclass declaration itself).

After this change, the class declaration for Pink would fail and no object of that type can be sent where a PrimaryColor is expected.

Considerations

There are some important aspects to keep in mind when working with sealed classes:

Sealed classes without permitted subtypes

Strictly speaking, a class or interface can be declared sealed without declaring the permitted subtypes, as long as the above considerations are taken into account. When this happens, the permitted subtypes are automatically inferred to those which are direct subtypes.

Care must be taken if this approach is used: sealed classes are usually intended to restrict a type hierarchy. If we don't explicitly list the possible subtypes by using the permits clause, then in future other subtypes can be added with unintended consequences, such as getting a compile-time error on a switch case because it's non-exhaustive anymore.

Final thoughts

Sealed classes are yet another great improvement to the Java language and platform, that gives developers more options to build secure robust and predictable software. It specifically allows us to model different kinds of a business domain in our code with precision and without resorting to weird workarounds or twisted class hierarchies.

Together with other language constructs, sealed classes, enhance our code and make it more succinct, readable, and maintainable, which leads towards more productive teams. If this articles doesn't cover everything about sealed classes, it is a good start for you to read and investigate more about this topic so you can put it in practice in your day-to-day job.


1 The colors system is complex. This post doesn't attempt to explain it in details. It doesn't either affirm that read, yellow, and blue are the primary colors.