Data Classes, Sealed Types and Pattern Matching

2024 01 01 head

Records, sealed types, enumerations are the key language features for Algebraic Data Types.

The features have been available since Java 17.

Latter JDK releases provide additional capabilities and syntactic sugar.

Based on the features of JDK 22, we are still missing with constructors for records and primitive support for pattern matching [1].

Pattern matching capabilities empower the developer to implement complex algorithms in a compact and legible way.

Patterns such as visitor pattern are obsolete. You can write the same functionality with one pattern matching switch case, and the compiler would validate completeness for you.

The compiler checks the exhaustiveness of the pattern matching. Source code is more maintainable and less error-prone.

Records

Records are shallow immutable data classes that require only the type and name of fields.

The developer has to take care of the deep immutability of the properties. But you are on your own. The compiler does not provide any support for deep immutability.

The compiler generates constructor, getters, equals, hashCode and toString.

public record Person(String firstname, String lastname, Address address) {}

The notation is compact and all properties are final. This approach provides shallow immutability. The developer has to take care of the immutability of the properties.

An additional capability of records is secure serialization.

Sealed Hierarchies

Design idiom Pattern matching Compiler checks.

sealed interface Expr permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    int eval(Expr e) {
        return switch (e) {
            case ConstantExpr(int i) -> i;
            case PlusExpr(Expr a, Expr b) -> eval(a) + eval(b);
            case TimesExpr(Expr a, Expr b) -> eval(a) * eval(b);
            case NegExpr(Expr e) -> -eval(e);                                             (1)
        };
    }
}

record ConstantExpr(int i) implements Expr { }
record PlusExpr(Expr a, Expr b) implements Expr { }
record TimesExpr(Expr a, Expr b) implements Expr { }
record NegExpr(Expr e) implements Expr { }
1 No need for a default case because the compiler verifies the exhaustiveness of the pattern matching.

The evaluation method is implemented using pattern matching. It replaces a complex visitor pattern implementation distributed over multiple classes.

Enumerations

Enumerations have been available since Java 5. Enumerations are a powerful language feature to define immutable objects.

The developer is responsible for creating immutable enumeration objects. It is possible to create mutable enumeration objects in Java.

They provide secure serialization and a perfect implementation for singletons.

Lessons Learnt

No more loops in Modern Java. Use Streams.

No more tests on null in Modern Java. Use optional monad.

Avoid Map and prefer records. Embrace immutable structures. Beware of shallow immutability versus deep immutability.

Pattern implementation in the functional world. No more visitor pattern. Use pattern matching.

These programming idioms are compliant with seminal books on software engineering [1, 2].

The next JDK releases will provide additional capabilities for pattern matching. Draft JEP for primitive types in patterns, instanceof, and switch was published and accepted in January 2024. Draft JEP for with constructors for records was also published in January 2024.

Step by step, the language is evolving to a more functional programming language. Syntax sugar for pattern matching is on the way and reduces boilerplate code.

References

[1] D. Farley, Modern Software Engineering. Pearson Education, Limited, 2022 [Online]. Available: https://www.amazon.com/dp/B09GG6XKS4

[2] J. Bloch, Effective Java, Third. Addison-Wesley Professional, 2017 [Online]. Available: https://www.amazon.com/dp/B078H61SCH


1. Primitive types in patterns, instanceof, and switch shall be provided as preview in JDK 23. A draft proposal for with constructors was published in the JEP list in January 2024.