Efficient and Immutable: How Java Records Enhance Your Code

Yogesh Bali
4 min readDec 16, 2024

--

In Java, a record is a special kind of abstract class available since Java 16.

It provides a concise way to create classes that are primarily used to store data, without having to write boilerplate code like constructors, getters, setters, equals(), hashCode(), and toString() methods.

The fields of a record are implicitly final, meaning that once a record object is created, its values cannot be changed.

Here’s a basic example of a record class in Java:

public record Person(String name, int age) {
}

Java automatically generates:

  • A constructor to initialize all fields.
  • Getter methods for each field.
  • equals(), hashCode(), and toString() methods.
public class RecordClassExample {

public static void main(String[] args) {
// Create an instance of the record
Person person = new Person("Rahul", 34);

// Access record fields
System.out.println("Name: " + person.name());
System.out.println("Age: " + person.age());

// Print the record instance
System.out.println(person);

// Create another record instance
Person anotherPerson = new Person("Sandeep", 52);

// Compare two records
System.out.println(person.equals(anotherPerson)); // false
}
}

Breakdown:

  • Constructor: The constructor is implicitly generated. In this case, the Person record has a constructor Person(String name, int age).
  • Accessors: You can access the fields via the automatically generated getter methods, e.g., person.name() and person.age().
  • toString(): The toString() method prints a human-readable form of the object: Person[name=Alice, age=30].
  • equals() and hashCode(): These methods are automatically overridden to compare the field values of two records.

What you can do with records:

  • Implement interfaces: A record can implement interfaces, and the interface methods must be implemented in the record.
  • Define non-abstract methods: You can define concrete methods in a record to add custom behavior.
  • Add constructors, static methods, and fields: You can add additional fields (though they must not conflict with the record components), static methods, and instance methods.

What you cannot do with records:

  • Records are final, No Inheritance and Abstract Methods: A record is implicitly declared final, which means it cannot be extended. Abstract methods are generally used in abstract classes that are intended to be subclassed, but records are not designed for inheritance in the traditional sense. Hence records do not support declaring an abstract method.
  • Records are Immutable, No Setters for Fields: Records are designed to have immutable fields (fields that cannot be modified after object creation). Setters are typically used to change the values of fields after the object has been instantiated. Since a record’s fields are immutable, it would not make sense to provide setters that could modify these fields.

Where to use records:

1. Data Transfer Objects (DTOs)
Use Case: When you need to transfer data between different layers of an application (e.g., between a client and server, or between different components of a system).
Why Use Records: Since records are immutable, they ensure that the data can’t be changed once it is created, making them a reliable way to transport data.

2. Immutable Data Models
Use Case: When you need to model data where the state should not change after it is created (e.g., configuration objects, value objects, or constants).
Why Use Records: Records automatically provide immutability and provide a compact way to define immutable objects, which is particularly useful in scenarios where data integrity and thread safety are important.

3. Error Handling or Result Wrapping
Use Case: When you want to wrap a result or an error in an immutable object, like in the case of returning a result that can either be a success or failure.
Why Use Records: Records are ideal for wrapping results because they provide a simple, immutable structure, which can be useful for returning success/error results in a clean way (e.g., wrapping a Success result or an Error result).

public record Result<T>(T value, String errorMessage) { }

4. Return Types for API Responses
Use Case: When you are developing an API and need to return a structured response, records are an excellent choice for defining the response object, especially when the response structure is fixed and immutable.
Why Use Records: The compact syntax and immutability of records make them a good choice for creating API responses that should not change after they are created, ensuring consistent and predictable data.

public record ApiResponse(String message, int statusCode) { }

5. Logging and Audit Records
Use Case: When you need to store data for logging or auditing purposes, where the data should not change once logged or audited.
Why Use Records: Since the data is usually collected at a specific point in time and should not change, records provide a clean and immutable structure for this purpose.

public record AuditLog(String action, String timestamp, String user) { }

When Not to Use Java Records:

  • Mutable Objects: If you need mutable fields or setters to modify the object’s state after creation, a regular class is more appropriate.
  • Complex Inheritance: If you require complex inheritance hierarchies (records cannot extend other classes or be extended themselves), a regular class would be better suited.

Records are ideal for data transfer objects, immutable models, error handling, configuration settings, and many other cases where you want to ensure data consistency and reduce boilerplate code

--

--

Yogesh Bali
Yogesh Bali

Written by Yogesh Bali

Senior Technical Lead in Thales

Responses (3)