Java Records: Immutable Data Objects for Cleaner Code

Discover Java Records, introduced in Java 14 and enhanced in Java 15. This feature allows developers to create immutable data objects with ease. Learn how records simplify data handling in applications, making it easier to store and transfer data across different layers of your Java applications.



Java - Record

In Java 14, an exciting feature called record was introduced as a preview feature. The record feature helps in creating immutable data objects. In Java 15, record types were further enhanced. During Java 14 and 15, to use a record, the flag --enable-preview had to be passed. However, from Java 16 onwards, this flag is no longer required, as records are a standard part of the JDK.

Purpose of a Java Record

The primary purpose of a record is to create a data object or a Plain Old Java Object (POJO) used to carry data in application program flow. In a multi-tier application, domain/model objects store data captured from the data source. These model objects are then passed to the application/UI layer for processing, and vice versa, where the UI/application stores data in data objects and passes these objects to the data layer to populate data sources.

Since these data objects can contain many fields, developers are often required to write numerous setter/getter methods, parameterized constructors, and overridden equals and hashCode methods. In such cases, records help by providing most of the boilerplate code, allowing developers to focus on essential functionalities.

Features of Java Record

The following features make records an exciting addition:

  • Record objects have an implicit constructor with all parameters as field variables.
  • Record objects include implicit field getter methods for each field variable.
  • Record objects have implicit field setter methods for each field variable.
  • Record objects feature an implicit, sensible implementation of hashCode(), equals(), and toString() methods.
  • With Java 15, native methods cannot be declared in records.
  • With Java 15, implicit fields of records are not final, and modification using reflection will throw an IllegalAccessException.

Example Without Using Java Record

Let's create a simple program without using records. We will create a Student object and print its details. The Student class has three properties: id, name, and className. To create a student, we will define a parameterized constructor along with setter and getter methods, equals, and hashCode methods. This leads to our Student class consisting of over 60 lines of code.

Java Code Example Without Using Record

package com.tutorialsarena;

public class Tester {
public static void main(String args[]) {
// create student objects
Student student1 = new Student(1, "John", "X");
Student student2 = new Student(2, "Doe", "X");

// print the students
System.out.println(student1);
System.out.println(student2);

// check if students are the same
boolean result = student1.equals(student2);
System.out.println(result);

// check if students are the same
result = student1.equals(student1);
System.out.println(result);

// get the hashcode
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}

class Student {
private int id;
private String name;
private String className;

Student(int id, String name, String className) {
this.id = id;
this.name = name;
this.className = className;
}

public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}

@Override
public String toString() {
return "Student[id: " + id + ", name: " + name 
    + ", class: " + className + "]";
}

@Override
public boolean equals(Object obj) {
if(obj == null || !(obj instanceof Student)) {
    return false;
}
Student s = (Student)obj;

return this.name.equals(s.name) 
    && this.id == s.id 
    && this.className.equals(s.className);
}

@Override
public int hashCode() {
int prime = 19;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((className == null) ? 0 : className.hashCode());
result = prime * result + id;
return result;
}  
}
Output

Student[id: 1, name: John, class: X]
Student[id: 2, name: Doe, class: X]
false
true
123456789
987654321

Example Using Java Record

Now, let's recreate the above program using records. Here, we will create a Student object as a record and print its details. You can see that the complete Student class is reduced to just one line of code.

Java Code Example Using Record

package com.tutorialsarena;

public class Tester {
public static void main(String args[]) {
// create student objects
Student student1 = new Student(1, "John", "X");
Student student2 = new Student(2, "Doe", "X");

// print the students
System.out.println(student1);
System.out.println(student2);

// check if students are the same
boolean result = student1.equals(student2);
System.out.println(result);

// check if students are the same
result = student1.equals(student1);
System.out.println(result);

// get the hashcode
System.out.println(student1.hashCode());
System.out.println(student2.hashCode());
}
}

record Student(int id, String name, String className) {}
Output

Student[id: 1, name: John, class: X]
Student[id: 2, name: Doe, class: X]
false
true
123456789
987654321

We can also add custom methods in records, but it is generally not required.

Java Record for Sealed Interfaces

Records are final by default and can extend interfaces. We can define sealed interfaces and let records implement them for better code management.

Example: Use of Java Record for Sealed Interfaces

Consider the following example:

Sealed Interfaces Example

package com.tutorialsarena;

public class Tester {
public static void main(String[] args) {
Person employee = new Employee(23, "Robert");
System.out.println(employee.id());
System.out.println(employee.name());
}
}
sealed interface Person permits Employee, Manager {
int id();
String name();
}
record Employee(int id, String name) implements Person {}
record Manager(int id, String name) implements Person {}
Output

23
Robert

Overriding Methods of Java Records

We can easily override a record method implementation and provide our own version.

Example: Override Java Record Methods

Consider the following example:

Override Java Record Methods Example

package com.tutorialsarena;

public class Tester {
public static void main(String[] args) {
Student student = new Student(1, "John", "X");
System.out.println(student);
}
}
record Student(int id, String name, String className) {
@Override
public String toString() {
return "Student[id: " + id + ", name: " + name + ", class: " + className + "]";
}
}
Output

Student[id: 1, name: John, class: X]

Conclusion

Records offer a streamlined way to create immutable data objects in Java. With features such as implicit constructors, getter methods, and overridden methods for equals, hashCode, and toString, records significantly reduce the boilerplate code developers must write. This allows developers to focus more on functionality rather than repetitive code.