Learn about record type in Java. It was introduced as a preview feature in Java 14 and finalized later. Java records should be used as immutable POJO to transfer data between classes and applications.
1. What is a Record?
Like enum
, a record is also a special class type in Java. It is intended to be used in places where a class is created only to act as a plain data carrier.
The important difference between class and record is that a record aims to eliminate all the boilerplate code needed to set and get the data from the instance (JEP-395). Records transfer this responsibility to the Java compiler, which generates the constructor, field getters, hashCode()
and equals()
as well toString()
methods.
Note that we can override any of the default provided above methods in record
definition to implement custom behavior.
2. How to Create a Record?
We need to use the keyword record
to create such record class in Java. Just like we do in constructors, we need to mention the attributes and their types in the record. In the given example, Employee is a record type and is used to hold employee information:
public record Employee(Long id, String firstName, String lastName, String email, int age){
}
To create a record, we call its constructor and pass all the field information in it. We can then get the record information using JVM-generated getter methods and call any of generated methods.
Employee e = new Employee(1l, "Lokesh", "Gupta", "howtodoinjava@gmail.com", 38);
System.out.println(e.id()); //1
System.out.println(e.email()); //howtodoinjava@gmail.com
System.out.println(e); //Employee[id=1, firstName=Lokesh, lastName=Gupta, email=howtodoinjava@gmail.com, age=38]
In the above example, when we create Employee
record, the compiler creates bytes code and includes following in the generated class file:
- An all-arguments constructor accepting all fields.
- The
toString()
method that prints the state/values of all fields in the record. - The
equals()
andhashCode()
methods using aninvokedynamic
based mechanism. - The getter methods whose names are similar to field names without the usual POJO/JavaBean
get
prefix i.e.id()
,firstName()
,lastName()
,email()
andage()
. - The class
extends java.lang.Record
, which is the base class for all records. It means a record cannot extend the other classes. - The class is marked
final
, so we cannot create a subclass. - It does not have any setter method, meaning a record instance is designed to be immutable.
If we run the javap
tool on generated class file, we will see the class file:
public final class com.howtodoinjava.core.basic.Employee extends java.lang.Record {
//1
public com.howtodoinjava.core.basic.Employee(java.lang.Long, java.lang.String, java.lang.String, java.lang.String, int);
//2
public java.lang.String toString();
//3
public final int hashCode();
public final boolean equals(java.lang.Object);
//4
public java.lang.Long id();
public java.lang.String firstName();
public java.lang.String lastName();
public java.lang.String email();
public int age();
}
3. Canonical, Compact, and Custom Constructors
JVM provides us an all-arguments constructor by default that assigns its arguments to the corresponding fields. It is called the canonical constructor. Like accessors methods, we can override this constructor and add custom logic such as data validation. It helps build a valid record in a given business context.
public record Employee(Long id, String firstName, String lastName, String email, int age) {
public Employee(Long id, String firstName, String lastName, String email, int age) {
Objects.requireNonNull(id);
Objects.requireNonNull(email);
if (age < 18) {
throw new IllegalArgumentException("You cannot hire a minor as employee");
}
}
//Other methods
}
The canonical constructor is again a boilerplate that we can avoid using the compact constructor. In compact constructors, we omit all arguments, including the parentheses. The components will be assigned to their respective fields automatically.
We can rewrite the above canonical constructor in a more compact form:
public record Employee(Long id, String firstName, String lastName, String email, int age) {
public Employee {
Objects.requireNonNull(id);
Objects.requireNonNull(email);
if (age < 18) {
throw new IllegalArgumentException("You cannot hire a minor person as employee");
}
}
//Other methods
}
A ‘compact constructor does not cause a separate constructor to be generated by the compiler. Instead, the code that you specify in the compact constructor appears as extra code at the start of the canonical constructor. We do not need to specify the assignment of constructor parameters to fields as it happens in the canonical constructor in the usual manner.
Like with classes, we can declare additional custom constructors, but any custom constructor must start with an explicit invocation of the canonical constructor as its first statement. It helps set the default values of all components not present in the new custom constructor.
public record Employee(Long id, String firstName, String lastName, String email, int age) {
public Employee(Long id, String firstName, String lastName){
this(id, firstName, lastName, "nobody@domain.com", 18);
//More code
}
//Other methods
}
4. Adding Fields and Methods
Adding a new field and method is possible, but not recommended. A new field added to record (not added to the component list) must be static. Adding a method is also possible that can access the internal state of record fields.
The added fields and methods are not used in implicitly generated byte code by the compiler, so they are not part of any method implementation such as equals()
, hashCode()
or toString()
. We must use them explicitly as required.
public record Employee(Long id, String firstName, String lastName, String email, int age) {
static boolean minor;
public boolean isMinor() {
return minor;
}
public String fullName() {
return firstName + " " + lastName;
}
public Employee {
if (age < 18) {
minor = true;
}
}
}
Now we can access the information similar to other Java classes:
Employee employee = new Employee(1l, "Amit", "Gupta", "email@domain.com", 17);
System.out.println(employee.isMinor()); //true
System.out.println(employee.fullName()); //Amit Gupta
5. Generic Records
Record types support generics, similar to other types in Java. A typical example of generic record is as follows;
record Container<T>(int id, T value) {
}
We can use this record as follows to support multiple types:
Container<Integer> intContainer = new Container<>(1, Integer.valueOf(1));
Container<String> stringContainer = new Container<>(1, "1");
Integer intValue = intContainer.value();
String strValue = stringContainer.value();
6. Serialization and Deserialization
Java serialization of records is different than it is for regular classes. The serialized form of a record object is a sequence of values derived from the final instance fields of the object. The stream format of a record object is the same as that of an ordinary object in the stream.
During deserialization, if the local class equivalent of the specified stream class descriptor is a record class, then first, the stream fields are read and reconstructed to serve as the record’s component values. Second, a record object is created by invoking the record’s canonical constructor with the component values as arguments (or the default value for the component’s type if a component value is absent from the stream).
The serialVersionUID
of a record class is 0L
unless it is explicitly declared. The requirement for matching serialVersionUID
values is also waived for record classes.
The process by which record objects are serialized cannot be customized; any class-specific writeObject, readObject
, readObjectNoData
, readResolve
, writeExternal
, and readExternal
methods defined by record classes are ignored during serialization and deserialization. However, the writeReplace
method may be used to return a substitute object to be serialized.
Before performing any serialization or deserialization, we must ensure that record must be either serializable or externalizable.
public record Employee (...) implements Serializable { }
Employee e = new Employee(1l, "Lokesh", "Gupta", "howtodoinjava@gmail.com", 38);
writeToFile(e, "employee1");
Employee temp = readFromFile("employee1");
static void writeToFile(Employee obj, String path) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path))) {
oos.writeObject(obj);
} catch (IOException e) {
e.printStackTrace();
}
}
static Employee readFromFile(String path) {
Employee result = null;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))) {
result = (Employee) ois.readObject();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return result;
}
7. Technical Deep Dive
7.1. invokedynamic
If we see the bytecode generated by the Java compiler to check the method implementations of toString()
(as well as equals()
and hashCode()
), they are implemented using an invokedynamic
based mechanism.
invokedynamic
is a bytecode instruction that facilitates the implementation of dynamic languages (for the JVM) through dynamic method invocation.
7.2. We cannot extend the Record class, explicitly
Though all records extend java.lang.Record
class, still we cannot create a subclass of java.lang.Record
explicitly. The compiler will not pass through.
final class Data extends Record { // Compiler error : The type Data may not subclass Record explicitly
private final int unit;
}
This means that the only way to get a record is to explicitly declare one and have javac
create the class file.
7.3. Using Annotations
We can add annotations to the components of a record. For example, we can apply @Transient
annotation to the id
field.
public record Employee(
@Transient Long id,
String firstName,
String lastName,
String email,
int age) {
// ...
}
Although keep in mind that records have automagically generated fields and component accessors. To support annotating these features, any annotations with the targets FIELD
, PARAMETER
, or METHOD
, are propagated to the corresponding locations if applied to a component.
In addition to the existing targets, the new target ElementType.RECORD_COMPONENT
was introduced for more fine-grained annotation control in Records.
8. JDK Changes
8.1. java.lang.Class
The Class class added two methods – isRecord()
and getRecordComponents()
. The getRecordComponents()
method returns an array of RecordComponent objects. The components are returned in the same order that they are declared in the record header.
RecordComponent
is a new class in the java.lang.reflect
package with eleven methods for retrieving things such as the details of annotations and the generic type.
8.2. ElementType enumeration
The ElementType enumeration has a new constant for records, RECORD_COMPONENT
.
8.3. javax.lang.model.element
The ElementKind
enumeration has three new constants for the records and pattern matching for instanceof features, namely BINDING_VARIABLE
, RECORD
and RECORD_COMPONENT
.
9. When to Use?
- Records are ideal candidates when modeling things like domain model classes (potentially to be persisted via ORM), or data transfer objects (DTOs).
- The records are useful when storing data temporarily. An example can be during JSON deserialization. Generally, we do not expect the program to mutate data read from JSON during deserialization. We just read the data and pass it to the data processor or validator.
- Also, records are not replacements for mutable Java beans because a record, by design, is immutable.
- Use records when a class is intended to hold the data for some time, and we want to avoid writing lots of boilerplate code.
- We can use records in various other situations e.g. hold multiple return values from a method, stream joins, compound keys and data structures such as tree nodes.
10. Conclusion
Java records are an extremely useful feature and a great addition to a Java-type system. It helps cut down boilerplate code written for simple data carrier classes almost completely.
But we should use it carefully and not try to customize its behavior. It is best used in default shape.If you ever start looking at ways to customize the record
type in your application, first consider o convert it to the simple class
type.
Happy Learning !!