34. Sealed Classes + Records as Algebraic Data Types
Combining sealed interfaces with records creates type-safe algebraic data types (sum types); exhaustive switch over the sealed type provides ML/Haskell-style pattern matching in Java.
Explanation
An algebraic data type (ADT) is a type that is exactly one of a fixed set of variants, each variant carrying its own data. In Java: a sealed interface defines the closed set of variants; record implementations define each variant's data.
The payoff: when you switch on a sealed type and handle every permitted subtype, the compiler verifies exhaustiveness โ no default needed, and adding a new variant causes compile errors at every switch site, forcing you to handle it. This makes refactoring safe.
This pattern models: result types (Ok/Err), AST nodes, JSON values, event types, state-machine states, and discriminated unions of any kind.
Example
public class AlgebraicDataTypes {
// JSON value ADT
sealed interface JsonValue permits
JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject {}
record JsonNull() implements JsonValue {}
record JsonBool(boolean value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonString(String value) implements JsonValue {}
record JsonArray(java.util.List<JsonValue> values) implements JsonValue {}
record JsonObject(java.util.Map<String, JsonValue> fields) implements JsonValue {}
static String prettyPrint(JsonValue val) {
return switch (val) {
case JsonNull() -> "null";
case JsonBool(var b) -> String.valueOf(b);
case JsonNumber(var n) -> n % 1 == 0 ? String.valueOf((long) n) : String.valueOf(n);
case JsonString(var s) -> "\"" + s + "\"";
case JsonArray(var vs) -> "[" + vs.stream()
.map(AlgebraicDataTypes::prettyPrint)
.reduce("", (a, b) -> a.isEmpty() ? b : a + ", " + b) + "]";
case JsonObject(var fs) -> "{" + fs.entrySet().stream()
.map(e -> "\"" + e.getKey() + "\": " + prettyPrint(e.getValue()))
.reduce("", (a, b) -> a.isEmpty() ? b : a + ", " + b) + "}";
};
}
public static void main(String[] args) {
var doc = new JsonObject(java.util.Map.of(
"name", new JsonString("Alice"),
"age", new JsonNumber(30),
"active", new JsonBool(true),
"scores", new JsonArray(java.util.List.of(
new JsonNumber(95), new JsonNumber(87)))
));
// Simple values
System.out.println(prettyPrint(new JsonNull()));
System.out.println(prettyPrint(new JsonBool(true)));
System.out.println(prettyPrint(new JsonString("Java")));
}
}Sources
- https://openjdk.org/jeps/409 โ JEP 409: Sealed Classes
- https://openjdk.org/jeps/395 โ JEP 395: Records
- https://dev.java/learn/pattern-matching/ โ Pattern matching guide