Skip to main content
pragmatic clean code minimizing cognitive load in production java

Records and Immutability

4 min read Chapter 9 of 25
Summary

Java Records provide native, immutable data carriers with...

Java Records provide native, immutable data carriers with automatic boilerplate reduction. Unlike Lombok @Value, Records are language-level features with shallow immutability, canonical constructors, and pattern matching support. They enforce data-carrying intent through finality, improve local reasoning, and offer safer serialization via constructor-only instantiation.

Records and Immutability

Introduction to Java Records

Java Records, introduced in Java 14 and finalized in Java 16, represent a significant enhancement to the Java language, particularly in the realm of data encapsulation and immutability. A Java Record is a special kind of class that is designed to be a transparent, immutable carrier for shallowly-immutable data. This means that while the fields of a record cannot be reassigned, the objects they reference may still be mutable. The primary goal of records is to reduce boilerplate code by automatically generating accessors, equals(), hashCode(), and toString() methods based on the record’s components.

Key Features of Java Records

  • Implicit Finality: Records are implicitly final and cannot extend other classes, ensuring they cannot be subclassed and thereby enforcing their immutability and data carrying intent.
  • Canonical Constructor: Each record has a canonical constructor whose signature matches the record’s components. This constructor is used to initialize the record’s fields.
  • Compact Constructor: Records can also have a compact constructor, which is a special syntax for constructors that allows for validation or normalization of the data being passed to the record without the need for an explicit parameter list and field assignments.
  • Shallow Immutability: While records themselves are immutable in terms of their field references, the objects those fields reference can still be mutable. For example, if a record contains a List field, the list itself can be modified even though the field reference in the record cannot be changed.
  • Pattern Matching: As of Java 21, records support deconstruction in pattern matching, allowing for more expressive and type-safe code when working with records.

Comparison with Lombok @Value

Lombok’s @Value annotation provides a way to create immutable classes with automatically generated boilerplate (getters, toString, equals, hashCode, and an all-args constructor). While both Java Records and Lombok @Value aim to simplify the creation of immutable data carriers, there are key differences:

  • Language Support: Java Records are a native language feature with JVM-level support, whereas Lombok @Value is a library-based solution that relies on annotation processing.
  • Inheritance: Records cannot extend other classes, whereas classes annotated with @Value can extend other classes if needed.
  • Syntax and Accessor Style: Records use a concise syntax in their declaration and have accessors that follow the name() convention, whereas @Value classes use standard class syntax and have accessors that follow the getName() convention.
  • Pattern Matching and Serialization: Records have built-in support for pattern matching and optimized serialization safety, features that @Value does not natively provide.

Implementing Immutable Data Carriers with Java Records

To create an immutable data carrier using Java Records, you define a record with the desired components. For example:

public record User(String id, String email) {
    public User {
        // Compact Constructor for validation
        if (id == null || email == null) {
            throw new IllegalArgumentException("Fields cannot be null");
        }
    }
}

This record ensures that User objects are immutable and provides automatic implementations for equals, hashCode, and toString based on the id and email fields. The compact constructor allows for validation, ensuring that neither id nor email can be null.

Benefits of Using Java Records for Immutability

  • Reduced Boilerplate: Records eliminate the need to manually write getters, equals, hashCode, and toString methods, reducing code verbosity and the chance for errors.
  • Enforced Immutability: By being implicitly final and having immutable field references, records enforce immutability at the language level, reducing the risk of unintended side effects.
  • Improved Readability: The concise syntax of records and their focus on data carrying make code easier to read and understand, as the intent of the class is clearly communicated.
  • Safer Serialization: Records provide safer serialization due to their immutable nature and the fact that they are always instantiated through their canonical constructor, reducing the risk of serialization-related bugs.

Conclusion

Java Records offer a powerful tool for creating immutable data carriers in Java, enhancing code readability, maintainability, and safety. By understanding and leveraging the features of Java Records, developers can write more expressive, efficient, and robust code, aligning with best practices for software development in the Java ecosystem.

Sources