Modern Java

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.

Java 17+ (sealed standard), Java 16+ (records standard)

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