Immutable Objects Using Record in Java


It is often useful to have objects that, once created, don’t change their content. To see a complete description on how to build such class, you can read my previous article “Immutable Objects in Java”.

Let’s imagine we want to build a PersonClass with two fields: firstName and lastName. To create immutable instances, this class must:

  • Have a constructor to init these fields
  • Keep the fields private and final to ensure they cannot be changed after being set in the constructor
  • Provide getter methods to access those fields
  • Be non-extendable, so we mark it as final
  • Override equals, hashCode, and toString methods, considering all fields

If we build such a class before Java 16, we would end up with something like the following 40-line code snippet…

package com.davidemarino;

import java.util.Objects;

public final class PersonClass {
    private final String firstName;
    private final String lastName;

    public PersonClass(String firstName, String lastName)  getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(preferences, that.preferences);
    

    public String getFirstName()  getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(preferences, that.preferences);
    

    public String getLastName()  getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName);
    

    @Override
    public boolean equals(Object o) 

    @Override
    public int hashCode() 

    @Override
    public String toString() {
        return "PersonClass';
    }
}

…and we can use it as follows:

package com.davidemarino;

public class Main {
    public static void main(String[] args) {
        PersonClass personClass = new PersonClass("John", "Doe");
        System.out.println("First name: " + personClass.getFirstName());
        System.out.println("Last name: " + personClass.getLastName());
        System.out.println(personClass);
    }
}

Executing it gives the following output:

First name: John
Last name: Doe
PersonClass{firstName="John", lastName="Doe"}

Much of the code needed to create an immutable class consists of boilerplate. But what do we really need to know? Just the class name and its fields.

Since Java 16, you can use the record keyword to define such a class.

A record is essentially a shortcut for creating:

  • a final class
  • with privatefinal fields
  • getter methods
  • a constructor to set all fields
  • overridden equals, hashCode, and toString methods

To define a record, use the record keyword (instead of final class) and declare the fields in the header, like this:

package com.davidemarino;

public record PersonRecord(String firstName, String lastName) { }

and we can use it as follows:

package com.davidemarino;

public class Main {
    public static void main(String[] args) {
        PersonClass personClass = new PersonClass("John", "Doe");
        System.out.println("First name: " + personClass.getFirstName());
        System.out.println("Last name: " + personClass.getLastName());
        System.out.println(personClass);

        PersonRecord personRecord = new PersonRecord("John", "Doe");
        System.out.println("First name: " + personRecord.firstName());
        System.out.println("Last name: " + personRecord.lastName());
        System.out.println(personRecord);
    }
}

As you can see, there is one key difference: the getter methods follow a different naming convention. Instead of getFirstName() and getLastName(), we have firstName() and lastName().

The rest of the code, however, remains identical.

But what happens if we have mutable object fields instead of strings?

In the traditional approach, we need to create a copy of the mutable field both in the constructor and in the getter method.

For example, let’s say we want to add a List preferences field to the PersonClass. The code should be updated like this:

package com.davidemarino;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class PersonClass {
    private final String firstName;
    private final String lastName;
    private final List preferences;
    
    public PersonClass(String firstName, String lastName, List preferences) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.preferences = new ArrayList<>(preferences);
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public List getPreferences() {
        return new ArrayList<>(preferences);
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(preferences, that.preferences);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, preferences);
    }

    @Override
    public String toString() {
        return "PersonClass{" +
                "firstName="" + firstName + "\'' +
                ", lastName="" + lastName + "\'' +
                ", preferences=" + preferences +
                '}';
    }
}

We can accomplish something similar using a record, as demonstrated below:

package com.davidemarino;

import java.util.ArrayList;
import java.util.List;

public record PersonRecord(String firstName, String lastName, List preferences) {
    public PersonRecord {
        preferences = new ArrayList<>(preferences);
    }

    public List preferences() {
        return new ArrayList<>(preferences);
    }
}

This part of the code is called a compact canonical constructor…

    public PersonRecord {
        preferences = new ArrayList<>(preferences);
    }

… equivalent to the following standard constructor.

    public PersonRecord(String firstName, String lastName, List preferences) {
        preferences = new ArrayList<>(preferences);
        this.firstName = firstName;
        this.lastName = lastName;
        this.preferences = preferences;
    }

So, to create a class for immutable instances, you can use the record construct, saving a lot of boilerplate code.

Conclusion

Creating immutable objects in Java traditionally required a lot of boilerplate code. With the introduction of records in Java 16, developers now have a concise and expressive way to define immutable data structures. Records automatically generate constructors, encapsulate fields, and override methods like equals, hashCode, and toString, significantly reducing manual coding effort. This makes code easier to read, maintain, and less error-prone. Overall, records are a powerful addition to Java’s type system, simplifying the creation of robust, immutable data models.


Share this content:

I am a passionate blogger with extensive experience in web design. As a seasoned YouTube SEO expert, I have helped numerous creators optimize their content for maximum visibility.

Leave a Comment