Hi Folks! (Notice: I created a Substack page recently to post my technical posts every week, if you're interested in what I written, let's subscribe it here https://phatng.substack.com/)
Java 14 introduced a new feature called Record, which is a special kind of class that can hold immutable data (data cannot be changed their value once it is assigned). Records are intent to be simple and concise ways to model data-only objects. In this blog post, we will discover the detail function of Records, how to use record
keyword, alongside with code examples to help you better understand it's functionality.
What are Records?
While working with Java application projects, especially web applications, we often write traditional classes likes controller classes, services classes… to process our business requirements. Throughout the business process, we also see the role of classes whose objective is only to carry data, I often treat these classes as Data Transfer Object. For example, the web application will receive the POST request from client, the request contains a request body in JSON format. When the web application receives the request, it transforms the JSON data into Java object — that object is the instance of DTO class.
When it comes to write such that classes, our developers often write the following things: fields, constructors, getters, setters, override some default Object’s methods: equals(), hashCode(), toString(),.. and maybe added some additional methods. Here is the example of Employee.java class
public class Employee { // fields private String id; private String name; private int level; private double salary; // constructors public Employee() {} public Employee(String id, String name, int level, double salary) { this.id = id; this.name = name; this.level = level; this.salary = salary; } // setters & getters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getLevel() { return level; } public void setLevel(int level) { this.level = level; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } // Object's methods @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return level == employee.level && Double.compare(employee.salary, salary) == 0 && id.equals(employee.id) && name.equals(employee.name); } @Override public int hashCode() { return Objects.hash(id, name, level, salary); } @Override public String toString() { return "Employee{" + "id='" + id + '\\'' + ", name='" + name + '\\'' + ", level=" + level + ", salary=" + salary + '}'; }
}
As you can see, it is bulky with a lot of boilerplate code. To eliminate boilerplate code, we can use Lombok — A useful Java library to make the code much shorter and more readable. Here is the Employee class with the help of Lombok.
@Data
public class Employee { // fields private String id; private String name; private int level; private double salary;
}
It’s much more elegant, right? And then in Java 14, the Record feature jumped in to change the game.
Java 14 introduced record
keyword to declare a Record class, let's create a similar class using Record to get its usage
public record EmployeeRecord(String id, String name, int level, double salary) {
}
With just 2 lines of code, we can create a class whose functionality equals to a traditional class with over 80 lines of code. What a concise syntax! 👏
How to use Records?
The general declaration is
record recordName(<<components>>) { //optional statements
}
We can see it’s more like a function declaration rather than that of a regular class. It has parentheses next to the name, and inside the parentheses, we provide list of components, which is quite similar to the fields of classes. Additionally, it can have optional statements inside curly brackets.
Now we are ready to create objects of record classes
EmployeeRecord empRecord = new EmployeeRecord("001", "John Doe", 3, 3000.0);
System.out.println(empRecord.id());
System.out.println(empRecord.toString());
System.out.println(empRecord.equals(empRecord));
System.out.println(empRecord.hashCode());
You may ask where the place for record is to put those toString(), equals(), hashCode(), as well as getter methods declaration. To answer, let's see the compiled class for EmployeeRecord
public final class EmployeeRecord extends java.lang.Record { private final java.lang.String id; private final java.lang.String name; private final int level; private final double salary; public EmployeeRecord(java.lang.String id, java.lang.String name, int level, double salary) { /* compiled code */ } public java.lang.String id() { /* compiled code */ } public java.lang.String name() { /* compiled code */ } public int level() { /* compiled code */ } public double salary() { /* 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 */ }
}
After observed the compiled class, we learn some points:
- The compiler compiled records into a final class that extends Record class.
- All records components are final, that’s why it stated records hold immutable data.
- The records class doesn’t have setter methods. Additionally, auto-generated getter methods don’t have “get” precedes their name and they only have public access. For example, to get id component, use
record.id()
- The records class also had toString(), hashCode(), equals() and there were auto-generated by the compiler.
At this time you should clear the magic behind record that performed by the compiler. Next we will discover some interesting parts of a record.
The specifications
Unlike regular classes, records do have some special specifications that you should know while working with them.
Constructors
Assumption that we have requirements for Employee class:
REQ#1 It required the level of an employee must be greater than 0, otherwise will throw an exception.
REQ#2 It required a constructor with only 3 fields: id, name, and level. The salary will be calculated by multiple base salary (1000) with current level.
Following is the implementation for this requirements in a regular class
public class Employee { // fields private String id; private String name; private int level; private double salary; private static final double BASE_SALARY = 1000.0; ... public Employee(String id, String name, int level, double salary) { if (level < 0) { throw new IllegalArgumentException("The level is invalid"); } this.id = id; this.name = name; this.level = level; this.salary = salary; } public Employee(String id, String name, int level) { this.id = id; this.name = name; this.level = level; this.salary = BASE_SALARY * this.level; } ...
}
For records, we can achieve REQ#1 with the help of special constructor called compact constructor
public record EmployeeRecord(String id, String name, int level, double salary) { // compact constructor public EmployeeRecord { if (level < 0) { throw new IllegalArgumentException("The level is invalid"); } }
}
Note that, for a compact constructor we don’t use parentheses following its name, and because of that, we don’t have to assign each component value with a corresponding constructor parameter (cuz we don’t have any constructor parameter 😂). In fact, it will automatically do the assignment. Also, we cannot use the this
keyword in compact constructor.
REQ#2 can be resolved using non-canonical constructor. But first, what is canonical constructor?
Basically it just a constructor with all record components. By default, records will implicit create a canonical constructor so that you can create instances of it. You realized something? Yes, the compact constructor is also a canonical constructor.
Records only have one canonical constructor
Non-canonical constructor is just a constructor without having all record components.
Back to our requirement, let's see the implementation
public record EmployeeRecord(String id, String name, int level, double salary) { // static component private static final double BASE_SALARY = 1000.0; // compact constructor //also canonical constructor public EmployeeRecord { if (level < 0) { throw new IllegalArgumentException("The level is invalid"); } } // non-canonical constructor public EmployeeRecord(String id, String name, int level) { this(id, name, level, BASE_SALARY * level); // must have a call to canonical constructor }
}
For non-canonical constructor, we must have a call to the canonical constructor at the first line. It is different with the normal constructor of class where the call to the other constructor is optional.
Other parts
In the previous code example, we see that a record can have static components, and it is the only component type allowed inside record’s body, it means we cannot have non-static component likes the following
public record EmployeeRecord(String id, String name, int level, double salary) { // non-static component is not allow // compiler will say: "Instance field is not allowed in record" private String title;
}
However, Instance methods are allowed in the record, consider this example
public record EmployeeRecord(String id, String name, int level, double salary) { // instance method is allowed public String getLevelTitle() { if (level == 1) { return "Entry level"; } else { return "High level"; } }
}
In addition, records can have instance static method as well. Despite cannot be extended and cannot extend any class, records can implement interfaces.
When to use Records?
Throughout our code examples, I believe you at least have a sense to know when we should use Records. You cannot use Records for Model/Entity classes because it often requires the state to be mutable, for example, we need to update the employee level then persist it into database.
Records are perfect for DTO classes, when we just need objects to be transferred over modules/network and do not contain a lot of boilerplate code.
In Spring Boot, developers can leverage records in configurations, request/response process, database result,…
Final thoughts
Records are not meant to replace classes as they still have a lot of limits, but rather to complement them. Records are useful when we need to present simple data structures which do not have any complicated logic or behavior. Records promote immutability and data encapsulation, yet still not good for securing your data (all components can be publicly accessible), but overall can improve readability and maintainability of code.
References
Java Record Class with Code Examples | Developer.com
https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Record.html