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.
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 {
}
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.
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
.
permits
keywordThe 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.
There are some important aspects to keep in mind when working with sealed classes:
sealed
as well — as you already might have inferred from previous
mentions to this.
sealed
without the permits clause and without another
class/interface implementing/extending it. So, for example, this code alone will report a compile-time error:
sealed interface Processor {
}
To make it compile we could have a class to implement it — something like this:
final class OrderProcessor implements Processor {
}
sealed
,
and none of its direct superinterfaces are sealed
, and it's neither sealed
nor final
itself. Otherwise, such a class is freely extensive if and only if it is declared
non-sealed
(and is not final
, of course).
sealed
direct superclass or a sealed
direct superinterface, it's a compilation error if such class is not declared final
,
sealed
, or non-sealed
, either explicitly or implicitly; that's because when a
class is declared sealed
, it forces all direct subclasses to explicitly declare whether
they are final
, sealed
, or non-sealed
.
final
or implicitly sealed
, so they can
implement a sealed interface. Similarly, a record class is implicitly final
, so it can
also implement a sealed interface.
non-sealed
, unless it has either a sealed direct superclass or a
sealed direct superinterface — otherwise a compilation error is reported.
sealed
modifier in the type declaration.
C
is associated with a named module (
§7.3
) , then every class specified in the permits clause of C
's declaration must be associated with
the same module as C
, or a compile-time error is reported
(§8.1.6).
This makes perfect sense, as supertypes and subtypes must be resolvable/accessible during compile-time
and for this to be 100% guaranteed, they must be located in the same module (in modular systems). Also, any
class hierarchy structure should be declared in the same business domain unit (module), where maintenance is
provided by the same development team.
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.
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.