2 min read

Algebraic Data Types (ADTs) in Java

Algebraic Data Types can be built using sealed classes.
Algebraic Data Types (ADTs) in Java

Sealed Classes and Interfaces

With the introduction of immutable record types in Java 14 and sealed classes in Java 17, you no longer need a framework to create ADTs. Record types are immutable classes. Sealed classes and interfaces allow you to restrict what can extend and implement them. This supports the ability to build ADTs.

How to Write an ADT

The following example shows a minimal Option pattern represented as an ADT. It follows the same flow as Java's Optional class, so it will be more familiar for Java developers.

package press.bytesize.adt;

import java.util.NoSuchElementException;

public sealed interface Option<T> {

    public record Some<T>(T value) implements Option<T> {
    }

    public record None() implements Option {
    }

    // Static factory method
    static <T> Option<T> ofNullable(final T value) {
        return switch (value) {
            case null -> new None();
            default -> new Some(value);
        };
    }

    default boolean isPresent() {
        return switch (this) {
            case Some some -> true;
            case None none -> false;
        };
    }

    default T get() {
        return switch (this) {
            case Some some -> (T) some.value();
            case None none -> throw new NoSuchElementException(
                    "No value present"
            );
        };
    }
}

This has two record types, Some and None. Since Some and None are defined inside Option, they are implicitly permitted to implement Option, which eliminates the need for a permits clause on Option. Since record types cannot extend a class, each record implements the Option interface. Methods are implemented on the Option interface so they can be used with both Some and None.

Using this pattern, you can create an Option one of three ways.

final Option option = Option.ofNullable("some");
final Some some = new Some("some");
final None none = new None();

Patterning Matching

Implementing ADTs using sealed classes, there is additional help the Java compiler will give. The Java compiler will ensure that all types are covered in a switch or contain a default label. If the switch doesn't cover all types or doesn't contain a default case, Java will generate a compiler error.

Using the Option class above, using pattern matching would look like the following:

package press.bytesize;

import press.bytesize.adt.Option;
import press.bytesize.adt.Option.*;

public class Main {

    public static void main(final String[] args) {
        final Option option = new Some("value");
        
        switch (option) {
            case Some s -> System.out.println("Some(" + s.value() + ")");
            case None n -> System.out.println("None");
        }
    }
}

Conclusion

You can create ADTs in Java now without the use of a framework. Using sealed classes and record types, the compiler gives you the help that you would expect in functional languages when working with ADTs.