Using Records in Modern Java Development
0 CommentsLast 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
- Getter and setter for each field
- A public constructor
- Override the
hashCode()
andequals()
methods of theObject
class - Override the
toString()
method of theObject
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 Student
class, 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 javap
command 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.
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.
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.
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.