Exploring the Power of Java Records: Simplifying Data Classes in Java

Exploring the Power of Java Records: Simplifying Data Classes in Java

Introduction

Sometimes as a Java programmer, writing a whole bunch of repetitive and boilerplate codes for a class representing a distinct object can be quite exhausting and stressful. You may be looking at it like, why can’t I substitute these similar code patterns with one line or two lines of code? This is where Records in Java come into play.
In this article, I will demonstrate with code examples how to convert our traditional POJO (Plain Old Java Object) class to a Record.

Prerequisites

To get along with this tutorial, you need the following tools installed on your local machine, and also a basic understanding of the following concepts:

  1. Java version 14 or later installed (JDK 14+)

  2. IntelliJ IDEA (Any IDE can be substituted for this, like Netbean, Eclipse, etc.)

  3. Basic understanding of Java (Variables, Data Types, etc.)

  4. Good knowledge of OOP (Object Oriented Programming) in Java

Concept of Java Record

What led to Java Record being introduced in Java 14? I am a fan of writing my Java classes for the sole purpose of holding data for an entity, which I want to believe most Java programmers also do. The idea behind Java Records is to aggregate (hold) data with a simple class definition with the class components, instead of the traditional style of writing POJO class which requires a couple of boilerplate code that includes: Private fields, Constructors, Getters, Setters, HashCode, Equals and toString methods.

It also raises the issue and is understandable why people have a much higher interest in picking up Kotlin and using Lombok dependency to reduce verbosity in Java. If you are observant enough, you will notice that IntelliJ IDEA and some other IDEs introduced shortcuts for generating these generic methods.

Writing a traditional POJO class

Let’s take a look at a simple POJO class defined as Client with its fields, constructors, setters, and getters methods, and a toString method

package entity;


import java.util.Objects;


public class Client {


   private long clientId;
   private String firstName;
   private String lastName;
   private String address;
   private String phone;
   private String email;
   protected Client() {
   }


   public Client(long clientId, String firstName, String lastName, String address, String phone, String email) {
       this.clientId = clientId;
       this.firstName = firstName;
       this.lastName = lastName;
       this.address = address;
       this.phone = phone;
       this.email = email;
   }


   public long getClientId() {
       return clientId;
   }


   public String getFirstName() {
       return firstName;
   }


   public void setFirstName(String firstName) {
       this.firstName = firstName;
   }


   public String getLastName() {
       return lastName;
   }


   public void setLastName(String lastName) {
       this.lastName = lastName;
   }


   public String getAddress() {
       return address;
   }


   public void setAddress(String address) {
       this.address = address;
   }


   public String getPhone() {
       return phone;
   }


   public void setPhone(String phone) {
       this.phone = phone;
   }


   public String getEmail() {
       return email;
   }


   public void setEmail(String email) {
       this.email = email;
   }


   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Client client = (Client) o;
       return clientId == client.clientId && Objects.equals(firstName, client.firstName) && Objects.equals(lastName, client.lastName)
               && Objects.equals(address, client.address) && Objects.equals(phone, client.phone) && Objects.equals(email, client.email);
   }


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


   @Override
   public String toString() {
       return "Client[" +
               "clientId=" + clientId + ", firstName=" + firstName  + ", lastName=" + lastName  + ", address=" + address +
               ", phone=" + phone + ", email=" + email  + ']';
   }
}

Hmmm, your guess is as good as mine. That was quite a large line of codes for a Client contract. You can observe from the code written above, that we have a lot of boilerplate codes, repeating similar styles of codes. Let us look at how you can modify these codes with a Record class.

Writing a Record class

You can reduce these code lines with a Record class. Let’s look at how you can use Java Record with the “record” keyword when creating a ClientRecord file. You will follow the same process of creating a class in Java but take note of the “record” keyword. The class is named ClientRecord.

package record_entity;


public record ClientRecord(long clientId, String firstName, String lastName, String address, String phone, String email) {

Yes!!! As you can see, you have reduced these lines of code to just a single class declaration with its parameters specified as the class contract.

Executing POJO and Java Record classes

Now let’s create a class with the main method to test if both classes will give us the same output when executed.

public class Main {


   public static void main(String[] args) {


       // Creating an Object of the Client class
       Client client1 = new Client(101, "Mark", "Anderson", "42 Java Street", "0001111", "mark.a@gmail.com");
       Client client2 = new Client(102, "Dean", "Becker", "42 Java Street", "0001111", "mark.a@gmail.com");


       // Displaying the Client Details
       System.out.println("First Client details: " + client1);
       System.out.println("Second Client details: " + client2);


       System.out.println();


       // Creating an Object of the ClientRecord class
       ClientRecord client3 = new ClientRecord(101, "Mark", "Anderson", "42 Java Street", "0001111", "mark.a@gmail.com");
       ClientRecord client4 = new ClientRecord(102, "Dean", "Becker", "42 Java Street", "0001111", "mark.a@gmail.com");


       // Displaying the ClientRecord Details
       System.out.println("First ClientRecord details: " + client3);
       System.out.println("Second ClientRecord details: " + client4);
   }
}

Successfully running the Main class will give us an output like this:

First Client details: Client[clientId=101, firstName=Mark, lastName=Anderson, address=42 Java Street, phone=0001111, email=mark.a@gmail.com]
Second Client details: Client[clientId=102, firstName=Dean, lastName=Becker, address=42 Java Street, phone=0001111, email=mark.a@gmail.com]

First ClientRecord details: ClientRecord[clientId=101, firstName=Mark, lastName=Anderson, address=42 Java Street, phone=0001111, email=mark.a@gmail.com]
Second ClientRecord details: ClientRecord[clientId=102, firstName=Dean, lastName=Becker, address=42 Java Street, phone=0001111, email=mark.a@gmail.com]

You can observe that the Record class works just the same way as the model (POJO) class in the previously written codes.

Important notes about Java Records

Before we move further, please note the following about record classes.

  • As you already know in Java, if a class is declared with a public keyword, it can be extended, but Record classes are final, so they cannot be extended.

  • Under the hood, Record classes are implicitly extended from java.lang.Record class.

  • All parameters or fields specified in the Record class are declared as final implicitly.

  • A single constructor is created with all the fields specified in the record definition.

  • The Record class automatically provides accessor methods for the fields. The method name is the same as the field name, not like generic and conventional getter methods.

  • Records have hashCode(), equals(), and toString() method implementations too, compared to a POJO class.

Checking for hashCode() and equals() methods in Java Records

Furthermore, an equals() method is generated for the Record class implicitly. The equals() method returns true if two or more objects of the same type have the same values for the fields.
If any of the values of the fields differ, the equals() method returns false.

Also, a corresponding hashCode() method is generated for the Record class, which is similar to the equals() method. The hashCode() method returns the same value (which is an Integer) for two or more objects if all of the field values of the objects are the same and match.

Let us check out the code snippets showing two objects of the ClientRecord added to the Main class having the same field values. You will inspect the hashCode() and equals() methods, and the outputs displayed on the console/terminal.

// Creating two objects of ClientRecord
ClientRecord client5 = new ClientRecord(104, "John", "Doe", "42 Java Street", "0001111", "mark.a@gmail.com");
ClientRecord client6 = new ClientRecord(104, "John", "Doe", "42 Java Street", "0001111", "mark.a@gmail.com");


// Checking for the two Object's hashCode
System.out.println("client5 hashCode is: " + client5.hashCode());
System.out.println("client6 hashCode is: " + client6.hashCode());


// Checking for Object's equals function
System.out.println("Are the two objects of the same type? " + client5.equals(client6));

Successfully executing the code snippets will give you the output as shown

client5 hashCode is: 363781449
client6 hashCode is: 363781449
are the two objects of the same type? true

Conclusion

In this article, you have been able to dive into the concept of the Record class, the reason why it is very important for aggregating and holding data in the class, and also, its simple class definition which differs from traditional POJO class. This tutorial also demonstrated how you can create and write your Record class, compare two objects using the equals() method, and check the two objects hashCode. You can find the code examples of this tutorial on GitHub.