Using Records in Modern Java Development

Using Records in Modern Java Development

0 Comments

Last Updated on October 21, 2024 by jt

Java 14 introduces a new feature called Records. In Java, Record is a special type of Java class. It is intended to hold pure immutable data in it. The syntax of a record is concise and short as compared to a normal class

In this post, I will explain why do we need Java records and how to use them.

Why Java Records?

Whenever you write a Java class, you have to add a lot of boilerplate code. Like

    1. Getter and setter for each field
    2. A public constructor
    3. Override the hashCode() and equals() methods of the Object class
    4. Override the toString() method of the Object class

So, if you have to create a Java class, say Student, you will have all these functions included.

An example Student class with boilerplate code is this.
Student.java

public class Student {

   private int id;
   private String firstName;
   private String lastName;
   private int grade;

   public Student() {
   }
   public Student(int id, String firstName, String lastName, int grade) {
      this.id = id;
      this.firstName = firstName;
      this.lastName = lastName;
      this.grade = grade;
   }

   public int getId() {
      return id;
   }

   public void setId(int id) {
      this.id = id;
   }

   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 int getGrade() {
      return grade;
   }

   public void setGrade(int grade) {
      this.grade = grade;
   }


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

   @Override
   public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      StudentClass that = (StudentClass) o;
      return id == that.id &&
          grade == that.grade &&
          Objects.equals(firstName, that.firstName) &&
          Objects.equals(lastName, that.lastName);
   }

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

As you can see in the Studentclass, we have getter and setter methods for every field. We have an empty constructor, a parameterized constructor, and so on.

If you are using an IDE, such as Intelli J these boilerplate codes can be generated. So, you as a programmer don’t need to type it on your own, but still, you will need to generate them. But, in the end, your class gets bulky and can cause readability issues for other developers.

The main advantage of using records is that the methods like equals()hashCode(), toString()constructor() are already generated. It makes the code short and easy to understand.

Record Syntax

The syntax of a Java Record modeling a Student is as follows.

public record Student(int id, String firstName, String lastName,int age, String PNo) {}

The preceding line of code is equivalent to the entire Student class I showed previously. This obviously saves a lot of time and reduces the boilerplate code.

Now, I have a Student record with four components: id, firstName, lastName, and grade.

Since Java Records is a preview language feature in JDK 14, you need to enable preview features to use them.  A preview language feature means that even though this feature is ready to be used by developers, it could be changed in a future Java release. They may either be removed in a future release or upgraded to permanent features, depending on the feedback received on this feature by developers.

So, to enable the Java 14 preview features you need to use --enable-preview -source 14 in the command line.

Now, let’s compile it like this.

javac --enable-preview --release 14 Student.java.

How to Use a Java Record?

A Java record can be used in the same way as a Java class.

Here is the code.

Student student1 = new Student(1,"Harry","styles",10);
Student student2 = new Student(4,"Louis","Tomlinson",11);

//to string
System.out.println(student1);
System.out.println(student2);

//accessing fields
System.out.println("First Name : " +student1.firstName());
System.out.println("Last Name : " +student1.lastName());
System.out.println(student1.toString());

//equals to
System.out.println(student1.equals(student2));

//hash code
System.out.println(student1.hashCode());

As you can see from the code without creating functions like hashCode(), equals() we can use them in records.

Now, let’s use the javapcommand to see what happens when a Record is compiled.

From command prompt/IntelliJ terminal, run javap Student.class

Here is the code of the decompiled Student class.

public final class Student extends java.lang.Record {
private final int id;
private final java.lang.String firstName;
private final java.lang.String lastName;
private final int grade;
public static java.lang.String UNKNOWN_GRADE

public Student(int id, java.lang.String firstName, java.lang.String lastName, int grade) {
/* compiled code */ }

public static java.lang.String getUnknownGrade() {/* compiled code */ }

public java.lang.String toString() {/* compiled code */}

public final int hashCode() {/* compiled code */}

public final boolean equals(java.lang.Object o) {/* compiled code */ }

public int id() {/* compiled code */ }

public java.lang.String firstName() {/* compiled code */ }

public java.lang.String lastName() {/* compiled code */}

public int grade() {/* compiled code */}
}

As you can see in the preceding code, no setter methods got created. This is because the type of record is final and immutable. Also, notice that the names of the getter methods are not preceded by get. Rather they contain the attribute name only.

More importantly, note that the Student class extends, java.lang.Record. All Java Records implicitly extend java.lang.Record class. However, you directly cannot extend the java.lang.Record class in your code.

The compiler will reject the attempt, like this:

$ javac --enable-preview -source 14 Student.java

Student.java:3: error: records cannot directly extend Record
public final class Student extends Record {
             ^
Note: Student.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
1 error

Also in the decompiled class, notice that a declares methods like equals()hashCode(), and toString() to be abstract. These abstract methods rely on invokedynamic to dynamically invoke the appropriate method which contains the implicit implementation. You can find more information on invokedynamic here.

Also, all the fields in the record declaration are specified as final.

Instance Methods in Record

Just like java classes, we can also include methods in a record definition. Here is an example of the Student Java Record definition from earlier sections. I have added an instance method named nameAsUpperCase().

public record Student(int id,String firstName,String lastName,int grade) {

public String nameAsUpperCase(){
return firstName.toUpperCase();
}

}

By simply invoking the function nameAsUpperCase() we will get the first name in the upper case.

The test code is this.

System.out.println("First Name : " +student1.nameAsUpperCase());

Let’s run the code and see the output.

instance method

Static Methods in Record

We can also add static methods and variables inside the record definition.

public record Student(int id, String firstName,String lastName,int grade) {
public static String UNKNOWN_GRADE = "grade not known" ;

public static String getUnknownGrade() {
    return UNKNOWN_GRADE;
 }
}

From the main class, we can call the getUnknownGrade() function.

The test code is this.

System.out.println(student1.getUnknownGrade());

The output is as follows.

static method

We can also add constructors inside the record definition. There are three types of record constructors. They are compact, canonical, and custom constructors.

A compact constructor doesn’t have any arguments. It doesn’t even have parenthesis.

This is a typical example of a compact constructor.

public Student{
if(id < 0)
    throw new IllegalArgumentException("student id cannot be negative");
}

Since id cannot be negative, I am adding an exception here.

This is the test code.

Student student = new Student(-1,"loius","lee",4);
System.out.println(student);

As you can see from the output we get an error message.

error message

A canonical constructor takes all the members as its parameters.

If you modify a canonical constructor, then it becomes a custom constructor.

You can only go for any one of the three constructors mentioned above at one time. This is because adding multiple constructors to a record like a regular class is not allowed.

Summary

Java Records is a great way to reduce boilerplate code without sacrificing the reliability of an immutable class.

If you have written code for enterprise applications, you might have encountered Lombok, a tool to also reduce boilerplate code.

There have been talks about Java Records replacing libraries like Lombok, but it is not so. Both are different tools for different things. There is some superficial overlap, but don’t let that distract you.

Lombok or similar boilerplate code generation libraries are largely about syntactic convenience. They are typically pre-loaded with some known useful patterns of code. They automate the patterns, according to the annotations you add to your class. Such libraries are purely about the convenience of implementing data-carrying classes.

On the other hand, Java Records are a semantic feature. They provide a first-class means for modeling data-only aggregates. They also are designed to close a possible gap in Java’s type system. In addition, as we saw, Records provide language-level syntax for a common programming pattern.

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.