Java sealed classes and exhaustive pattern matching
Java 17 introduced a feature called sealed classes, which enable developers to explicitly list the allowed sub-types of an interface or base class. To illustrate, consider a sealed interface paired with records, where implicit inner classes are added to the permitted sub-types if no explicit list is provided. This concept is akin to declaring a datatype in functional programming languages like Haskell or ML.
An interesting aspect of sealed classes is that they allow for exhaustiveness checking. Normally, when using a switch expression, a default clause is required to account for all possible cases. However, with sealed classes, the compiler knows the exact cases and can verify if they've all been covered. If so, the default clause becomes unnecessary. If a case is missed and no default clause is provided, the compiler throws an error, ensuring all potential cases are handled.
While this feature is beneficial for ensuring comprehensive handling of types, it also introduces a new type of breaking change. Adding a new sub-type to a sealed class or interface can break existing code that uses it. For instance, adding a new 'TypeC' case to our example would cause the main method to fail to compile due to the overlooked case.
Consequently, exporting a sealed type in an API implies that adding a new subtype constitutes a breaking change, necessitating a major version bump according to Semantic Versioning principles.
It's worth noting that while the compiler can detect non-exhaustive switches during compilation, this isn't always possible if the consumer (the code using the sealed type) hasn't been recompiled alongside the sealed type. For instance, if we extend 'SealedType' with an additional case, recompiling 'SealedType.java' without updating 'Main' would result in a runtime exception if the new case is triggered.
Despite these considerations, the Javadoc acknowledges the potential for runtime exceptions due to separate compilation issues and mentions corner cases involving nulls in patterns. Thus, while sealed classes offer compile-time safety guarantees for internal use within a component, their utility diminishes when exposed in public APIs offered to other components, such as libraries.
In such cases, the risk of breaking code increases, often detectable only at runtime. This insight was uncovered while reviewing the preview support for PEM-encoded cryptographic objects, which inadvertently incorporated a sealed interface into a public API, potentially leading to significant breakage if new cases were added.
Written by urgent.news from Hacker News's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.