Skip to main content

JAVA is one of the most popular and widely used programming languages, especially for enterprise application development. With billions of devices running on JAVA, it has proven itself as a reliable and versatile language. Oracle, the company responsible for developing and maintaining JAVA, constantly introduces new features to make the language more user-friendly and maintainable. One of these cool features is JAVA Records, which I am going to discuss in this blog. In many cases, we use classes to define objects that hold data, such as query results from databases or information that needs to be stored and retrieved. To ensure the validity of the data and maintain consistency, we often need to make these classes immutable. Achieving immutability in traditional classes involves writing boilerplate code, which can be time-consuming and error-prone.

Traditional Approach

Let's consider a simple class named Customer that has two fields: firstName and lastName. If we want to make this class immutable, we need to follow a set of steps:

  • Create private final fields for each piece of data.
  • Create getter methods for each field.
  • Create a public constructor that takes corresponding arguments for each field.
  • Override the equals() method to compare if all fields of the class match.
  • Override the hashCode() method to return a consistent value when all field values match.
  • Override the toString() method to provide a string representation of the class and its fields.

Here is an example of how the Customer class would look using the traditional approach:

public class Customer {

    private final String firstName;

    private final String lastName;

   

    public Customer(String firstName, String lastName) {

        this.firstName = firstName;

        this.lastName = lastName;

    }

   

    public String getFirstName() {

        return firstName;

    }

   

    public String getLastName() {

        return lastName;

    }

   

    @Override

    public int hashCode() {

        return Objects.hash(firstName, lastName);

    }

   

    @Override

    public boolean equals(Object obj) {

        if (this == obj) {

            return true;

        } else if (!(obj instanceof Customer)) {

            return false;

        } else {

            Customer other = (Customer) obj;

            return Objects.equals(firstName, other.firstName)

                    && Objects.equals(lastName, other.lastName);

        }

    }

   

    @Override

    public String toString() {

        return "Customer{" +

                "firstName='" + firstName + '\'' +

                ", lastName='" + lastName + '\'' +

                '}';

    }

}

While the traditional approach achieves immutability, it has some drawbacks:

  • It requires writing a lot of boilerplate code.
  • The main purpose of the class, describing a Customer and their first name and last name, gets a bit hidden.
  • Adding new fields to the class requires manually updating the code, making it error-prone.
  • It makes the code more complicated than necessary for a simple class with just two fields.

To address the drawbacks of the traditional approach, Java introduced Records. Records are essentially immutable data classes that only require the field type and field name. The Java compiler automatically generates the getter methods, hashCode(), equals(), and toString() methods for records.

To create a record for our Customer, we can simply declare it as follows:

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

The Java compiler automatically generates a public constructor for the record. We can use this constructor to initialize the record, just like we would with a traditional class.

Customer customer = new Customer("John", "Doe");

The compiler also generates getter methods for the record fields. We can access these methods to retrieve the values.

String firstName = customer.firstName();

String lastName = customer.lastName();

The equals() method is automatically created for records. It returns true if the object given is of the same record type and all field values match.

Customer customer1 = new Customer("John", "Doe");

Customer customer2 = new Customer("John", "Doe");

assertTrue(customer1.equals(customer2)); // Returns true

Similar to the equals() method, the hashCode() method is also automatically created for records. The hashCode() method returns the same value when two records have matching field values.

Customer customer1 = new Customer("John", "Doe");

Customer customer2 = new Customer("John", "Doe");

assertEquals(customer1.hashCode(), customer2.hashCode()); // Returns true

The toString() method provides a string representation of the record, including the record's name and each field's name and value in square brackets.

Customer customer = new Customer("John", "Doe");

System.out.println(customer); // Outputs: Customer[firstName=John, lastName=Doe]

Although a public constructor is automatically generated for records, we can customize it according to our needs. We can add additional checks or create different constructors with alternative parameter combinations.
For example, we can ensure that the first name and last name provided to the constructor are not null:

public Customer(String firstName, String lastName) {

    this.firstName = Objects.requireNonNull(firstName);

    this.lastName = Objects.requireNonNull(lastName);

}

We can also create a constructor with just the first name, assuming a blank string as the last name:

public Customer(String firstName) {

    this(firstName, "");

}

Java Records provide a more efficient and concise way of writing immutable classes. By eliminating the boilerplate code, Records make the code cleaner and easier to read. The auto-generated methods streamline the development process and ensure consistency. With Java Records, developers can focus more on the main purpose of their classes and reduce the chances of errors. So, the next time you need to create immutable data classes in Java, give Java Records a try and experience the benefits firsthand.

Integrate People, Process and Technology