Sealed Classes in Java

Sealed Classes in Java

Author image
by Sven WoltmannDecember 28, 2021

Article Series: Project Amber

In Project Amber, new Java language features are being developed and gradually released.

Part 1: Switch Expressions (Java 14)

Part 2: Text Blocks (Java 15)

Part 3: Records (Java 16)

Part 4: Sealed Classes (Java 17)

(Sign up for the HappyCoders Newsletter
to be immediately informed about new parts.)

Sealed classes and interfaces were the big news in Java 17.

In this article, you will learn:

  • What are sealed classes and interfaces?
  • How exactly do sealed classes and interfaces work?
  • What do we need them for?
  • Why should we restrict the extensibility of a class hierarchy?

Let's start with an example...

Starting Point: Example Class Hierarchy

Let the following class hierarchy be the starting point:

Sealed classes example - initial situation
Sealed classes example - initial situation

Here is the Java source code for the example:

public class Shape { ... } public class Circle extends Shape { ... } public class Rectangle extends Shape { ... } public class Square extends Shape { ... } public class WeirdShape extends Shape { ... } public class TranspRectangle extends Rectangle { ... } public class FilledRectangle extends Rectangle { ... }
Code language: Java (java)

Usually, every developer can extend this class hierarchy at any place. An extended structure could look like this (I've colored the added classes light yellow):

Sealed classes example - extension possibilities without sealing
Sealed classes example - extension possibilities without sealing

Now, we may want to restrict the extension of our class hierarchy. For example, we might want to specify that developers may only extend the WeirdShape class.

Why would we want to do that, and how can we do it?

Why Restrict the Extensibility of a Class Hierarchy?

There may be several reasons why we want to restrict the free extensibility of our class hierarchy:

  • We want to protect the internal state of a class or a hierarchy of classes and not have it manipulated inconsistently by a child class.
  • We want to protect internal objects whose thread safety is guaranteed by our class or class hierarchy from being published so that foreign code cannot compromise thread safety.
  • We want to ensure that the Liskov substitution principle (LSP) is not violated. That is, we don't want a developer to implement a derived class that breaks the API contract of the parent class.

Now that we know the reasons for constraining a class hierarchy, we move on to the next question: How can we do that?

Sealing the Class Hierarchy – Step by Step

We already know the first possibility…

Restricting the Class Hierarchy with "final"

By marking classes as "final", we can prevent their extension in general.

A second option would be to mark a class as package-private to permit only subclasses within the same package. However, this would have the consequence that the superclass would no longer be visible outside the package, which is undesirable in most cases.

Let's try to use "final" in our example. We mark the classes Circle, TranspRectangle, FilledRectangle, and Square as final (remember: WeirdShape should remain the only extendable class).

This limits the extensibility of our class hierarchy, as shown in the following class diagram:

Restricting the class hierarchy with "final"
Restricting the class hierarchy with "final"

To improve clarity, I have removed the crossed-out boxes below the final classes in the following graphic:

Restricting the class hierarchy with "final"
Restricting the class hierarchy with "final"

This means we are on the right track, but we are still far from our goal. What now? Obviously, we can't make Shape and Rectangle final because other classes should extend them.

This is where sealed classes come in...

Sealing the Class Hierarchy with "sealed" and "permits"

Using "sealed types", we can implement what is called a "sealed class hierarchy". It works as follows:

  • We mark the class whose subclasses we want to restrict with the sealed keyword.
  • Using the keyword permits, we list the allowed subclasses.

We extend the code of the Shape and Rectangle classes as follows:

public sealed class Shape permits Circle, Square, Rectangle, WeirdShape { ... } public sealed class Rectangle extends Shape permits TranspRectangle, FilledRectangle { ... }
Code language: Java (java)

With this code, we state the following:

  • The Shape class may only be extended by the Circle, Square, Rectangle and WeirdShape classes.
  • The Rectangle class may only be extended by the TranspRectangle, and FilledRectangle classes.

The following class diagram shows the constraints added by sealed and permits:

Restricting the class hierarchy with "sealed" and "permits"
Restricting the class hierarchy with "sealed" and "permits"

For the sake of clarity, once more without the crossed-out classes:

Restricting the class hierarchy with "sealed" and "permits"
Restricting the class hierarchy with "sealed" and "permits"

It looks like we have reached our goal. But one step is still missing…

Opening the Sealed Class Hierarchy with "non-sealed"

With the changes made so far, our code looks like this:

public sealed class Shape permits Circle, Square, Rectangle, WeirdShape { ... } public final class Circle extends Shape { ... } public sealed class Rectangle extends Shape permits TranspRectangle, FilledRectangle { ... } public final class Square extends Shape { ... } public class WeirdShape extends Shape { ... } public final class TranspRectangle extends Rectangle { ... } public final class FilledRectangle extends Rectangle { ... }
Code language: Java (java)

When we try to compile this code, we get the following error message:

$ javac *.java WeirdShape.java:3: error: sealed, non-sealed or final modifiers expected public class WeirdShape extends Shape { ^
Code language: plaintext (plaintext)

For preventing accidental openings of the sealed class hierarchy, all classes must be marked sealed, non-sealed, or final.

Our WeirdShape class should be extensible, i.e., the sealing should be opened at this class. Therefore we have to mark this class as non-sealed:

public non-sealed class WeirdShape extends Shape { ... }
Code language: Java (java)

Our final class hierarchy thus looks like this:

Opening the sealed class hierarchy with "non-sealed"
Opening the sealed class hierarchy with "non-sealed"

Particularities

There are some particularities to keep in mind when using sealed class hierarchies.

Sealing within a "Compilation Unit"

The permits keyword can be omitted if subclasses derived from a sealed class are defined within the same class file ("compilation unit"). These are then considered "implicitly declared permitted subclasses".

In the following example, ChildInSameCompilationUnit is such a subclass; therefore, the permits keyword may be omitted:

public sealed class SealedParentWithoutPermits { public final class ChildInSameCompilationUnit extends SealedParentWithoutPermits { // ... } }
Code language: Java (java)

Local Classes

Local classes (i.e., classes defined within methods) must not extend sealed classes.

The following code shows a local class extending an unsealed class. This code is valid:

public class NonSealedParent { public void doSomething() { class LocalChild extends NonSealedParent { // Allowed // ... } // ... } }
Code language: Java (java)

However, if the outer class is sealed, the local class may not inherit from it:

public sealed class SealedParent { public void doSomething() { class LocalChild extends SealedParent { // Not allowed // ... } // ... } }
Code language: Java (java)

instanceof Tests with Sealed Classes

For instanceof tests, the compiler checks whether the class hierarchy allows the check ever to return true. If it does not, the compiler reports an "incompatible types" error, such as in the following code:

Number n = getNumber(); if (n instanceof String) { // Not allowed // ... }
Code language: Java (java)

A Number object can never be an instance of String. The compiler therefore reports:

incompatible types: Number cannot be converted to String

Information from sealed class hierarchies is also included in this check. What this means is best explained with an example:

Let's assume we have an interface A and a class B:

interface A {} class B {}
Code language: Java (java)

Thus, the following check is valid:

public boolean isAaB(A a) { return a instanceof B; }
Code language: Java (java)

How can this check return true? By defining a class C that inherits from B and implements A:

class C extends B implements A {}
Code language: Java (java)

The invocation of isAaB(new C()) then returns true.

Now we seal interface A and allow only AChild as a subclass; we leave class B unchanged:

sealed interface A permits AChild {} final class AChild implements A {} class B {}
Code language: Java (java)

The compiler now recognizes that an object of type A can never also be an instance of B. Accordingly, the check if (a instanceof B) is from now on acknowledged with the following compiler error:

incompatible types: A cannot be converted to B

Contextual Keywords

The introduction of new keywords such as sealed, non-sealed, permits (or yield from switch expressions) raised the following question among JDK developers: What should happen to existing code that uses these keywords as method or variable names?

Since Java places a high value on backward compatibility, it was decided not to affect existing code as much as possible. That is made possible by so-called "contextual keywords" – keywords that only have a meaning in a specific context.

The terms sealed and permits, for example, are such "contextual keywords" and have meaning only in the context of a class definition. In other contexts, they can be used as method or class names. So the following is valid Java code:

public void sealed() { int permits = 5; }
Code language: Java (java)

Completeness Analysis for "Pattern Matching for switch"

In Java 17, "Pattern Matching for switch" was introduced as a preview feature. In combination with this feature, sealed classes allow exhaustion analysis, i.e., the compiler can check whether a switch statement or expression covers all possible cases.

Here is a small class hierarchy with a sealed interface as the root:

public sealed interface Color permits Red, Blue {} public final class Red implements Color {} public final class Blue implements Color {}
Code language: Java (java)

"Pattern Matching for switch" enables code like the following:

Color color = getColor(); switch (color) { case Red r -> ... case Blue b -> ... }
Code language: Java (java)

The compiler recognizes that the object color can only be an instance of Red or Blue; thus, the switch statement is complete and does not require a default case.

Another advantage is that the compiler will point out the missing switch case if the class hierarchy is extended later.

Let's extend our class hierarchy with the color green (don't forget to extend the permits list of Color):

public sealed interface Color permits Red, Blue, Green {} public final class Red implements Color {} public final class Blue implements Color {} public final class Green implements Color {}
Code language: Java (java)

When trying to recompile the switch statement, the compiler aborts with the following error message:

$ javac --enable-preview -source 17 SwitchTest.java SwitchTest.java:6: error: the switch statement does not cover all possible input values switch (color) { ^
Code language: plaintext (plaintext)

So, with sealed class hierarchies, the compiler can help us avoid incomplete switch statements or expressions – a common cause of errors when extending class hierarchies.

Conclusion

Sealed classes were introduced by JDK Enhancement Proposal 409 in Java 17. They allow us to protect a class hierarchy from unwanted extensions.

For "Pattern Matching for switch", introduced as a preview feature in 17, they also enable completeness analysis.

Sealed Classes were developed along with other new language features such as records, switch expressions, text blocks, and pattern matching in Project Amber.

If you liked the article, feel free to leave me a comment or share the article using one of the share buttons at the end.

Want to be notified when new articles are published? Then click here to sign up for the HappyCoders newsletter.