DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Java String: A Complete Guide With Examples
  • Generics in Java and Their Implementation
  • The Two-Pointers Technique
  • Mule 3 DataWeave(1.x) Script To Resolve Wildcard Dynamically

Trending

  • The Request Timed Out, But the Payment Succeeded: Building Retry-Safe Mobile APIs
  • Common REST Design Pattern
  • Android Application Security: Android Application Architecture
  • Multi-Agent Orchestration on AWS With AgentCore Runtime and A2A

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Data Engineering
  3. Data
  4. Immutable Objects Using Record in Java

Immutable Objects Using Record in Java

Java 16 records simplify immutable class creation by automatically generating constructors, getters, and methods, reducing boilerplate code significantly.

By 
Davide Lorenzo Marino
·
Jul. 30, 25 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
7.0K Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

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...

Java
 
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) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    @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);
    }

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

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


...and we can use it as follows:

Java
 
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:

Plain Text
 
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:

Java
 
package com.davidemarino;

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


and we can use it as follows:

Java
 
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<String> preferences field to the PersonClass. The code should be updated like this:

Java
 
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<String> preferences;
    
    public PersonClass(String firstName, String lastName, List<String> preferences) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.preferences = new ArrayList<>(preferences);
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public List<String> 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:

Java
 
package com.davidemarino;

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

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

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


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

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


... equivalent to the following standard constructor.

Java
 
    public PersonRecord(String firstName, String lastName, List<String> 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.

Data structure Java (programming language) Strings

Opinions expressed by DZone contributors are their own.

Related

  • Java String: A Complete Guide With Examples
  • Generics in Java and Their Implementation
  • The Two-Pointers Technique
  • Mule 3 DataWeave(1.x) Script To Resolve Wildcard Dynamically

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook