Java sealed classes and exhaustive pattern matching
Understanding Java Sealed Classes and Exhaustive Pattern Matching
Originally discussed by Neil Madden, the introduction of sealed classes in Java 17 provides a mechanism to restrict which classes or interfaces can extend or implement a specific base type.
The Core Concept
A sealed interface allows a developer to define a closed set of permitted subtypes. This is particularly useful when the domain of a type is strictly limited.
Implementation Example
Below is a basic demonstration using a sealed interface and records. Note that when records are defined within the same file, they are implicitly permitted.
public sealed interface SealedType {
record TypeA() implements SealedType {}
record TypeB() implements SealedType {}
static SealedType of(String type) {
return switch (type) {
case "A" -> new TypeA();
case "B" -> new TypeB();
default -> throw new IllegalArgumentException();
};
}
}
Functional Programming Parallel
If you have experience with languages like ML or Haskell, this structure is essentially an Algebraic Data Type (ADT). In Haskell, the equivalent declaration would look like this:
data SealedType = TypeA | TypeB
Mathematically, we can represent the set of possible types as:
Exhaustiveness Checking
The real power of sealed classes manifests when using them in switch expressions. Consider the following Main class:
void main(String[] args) {
var val = SealedType.of(args[0]);
System.out.println(switch (val) {
case SealedType.TypeA() -> "A";
case SealedType.TypeB() -> "B";
});
}
The most striking detail here is that we did not need a . Because the interface is default clausesealed, the compiler performs exhaustiveness checking. It knows that TypeA and TypeB are the only possibilities; if they are all covered, the code is valid.
Visualizing the Hierarchy
The Risks: Breaking Changes and Compilation
While exhaustiveness is a benefit, it introduces a specific risk regarding API stability.
1. The SemVer Problem
Adding a new subtype (e.g., TypeC) to a sealed interface is a breaking change. Any consumer using a switch expression without a default clause will suddenly face a compile-time error. According to Semantic Versioning (SemVer), this would necessitate a major version bump.
2. The Separate Compilation Trap
A more insidious issue arises when the consumer is not recompiled. If you update the library but the client continues using the old .class files for their Main method, the compiler cannot warn them.
Scenario:
- Update
SealedTypeto includerecord TypeC(). - Recompile
SealedType.javaonly. - Run the existing
Mainclass with input"C".
Result:
% java Main C
Exception in thread main java.lang.MatchException at Main.main(Main.java:3)
The java.lang.MatchException is thrown at runtime because the bytecode for Main does not account for the new case.
Summary Comparison
| Feature | Standard Interface | Sealed Interface |
|---|---|---|
| Subtype Control | Open to anyone | Explicitly restricted |
| Switch Requirements | Requires default | Exhaustive (no default needed) |
| API Stability | High (easy to add types) | Lower (adding types breaks clients) |
| Safety | Runtime checks | Compile-time checks (if recompiled) |
Final Verdict
"Sealed types are most effective when used within the internal implementation of a component. They are significantly more dangerous when exposed in a public API offered to other libraries."
When used internally, you typically recompile the entire project, gaining full type safety. When used across module boundaries, you risk runtime failures.
Real-world Caution: The author noted this exact pitfall in the preview support for PEM-encoded cryptographic objects. By baking a sealed interface into a public API and encouraging pattern matching, the designers created a high probability of future breakage.
Checklist for using Sealed Classes:
- Is this type used only internally? Safe to use.
- Is this part of a public API? Proceed with caution.
- Will consumers be forced to recompile? Necessary for safety.
- Am I prepared for a major version bump if I add a case? Required for SemVer.
