Learn about record type in java. It is introduced as preview feature in Java 14 and shall be used as plain immutable data classes for data transfer between classes and applications.
Table of Contents 1. 'record' type 2. When we should use records 3. Technical deep dive 4. API Changes 5. Conclusion
1. ‘record’ type
Like enum
, 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 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 instance. Records transfer this responsibility to java compiler which generates the constructor, field getters, hashCode()
and equals()
as well toString()
methods.
We can override any of default provided above methods in record
definition to implement custom behavior.
1.1. Syntax
Use the keyword record
to create such record class in Java. Just like we do in constructors, we need to mention the attributes and theirs types in record.
In given example, EmployeeRecord
is used to hold a employee information i.e.
package com.howtodoinjava.core.basic; public record EmployeeRecord(Long id, String firstName, String lastName, String email, int age) { }
1.2. Create and use records
To create a record, 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.
package com.howtodoinjava.core.basic; public class RecordExample { public static void main(String[] args) { EmployeeRecord e1 = new EmployeeRecord (1l, "Lokesh", "Gupta", "howtodoinjava@gmail.com", 38); System.out.println(e1.id()); System.out.println(e1.email()); System.out.println(e1); } }
Program output:
1 howtodoinjava@gmail.com EmployeeRecord[id=1, firstName=Lokesh, lastName=Gupta, email=howtodoinjava@gmail.com, age=38]
1.3. Under the hood
When we create EmployeeRecord
record, the compiler creates bytes code and include following things in generated class file:
- A constructor accepting all fields.
- The
toString()
method which 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 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 other classes. - The class is marked
final
, which means we cannot create a subclass of it. - It does not have any setter method which means 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.EmployeeRecord extends java.lang.Record { //1 public com.howtodoinjava.core.basic .EmployeeRecord(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(); }
2. When we should use records
- Records are ideal candidate 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. As example can be during JSON deserialization. Generally during deserialization, we do not expect the program to mutate data read from JSON. We just read the data and pass it to data processor or validator.
- Also, records are not replacement 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 variety of other situations e.g. hold multiple return values from a method, stream joins, compound keys and in data-structure such as tree nodes.
3. Technical deep dive
3.1. invokedynamic
If we see the bytecode generated by 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.
3.2. Cannot subclass Record 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 { private final int unit; } // Compiler error : The type Data may not subclass Record explicitly
This means that the only way to get a record is to explicitly declare one and have javac
create the class file.
3.3. Using annotations
we can add annotation to the components of a record, which are applicable to them. For example, we can apply @Transient
annotation to the id
field.
public record EmployeeRecord( @Transient Long id, String firstName, String lastName, String email, int age) { // }
3.4. Serialization
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; and second, a record object is created by invoking the record’s canonical constructor with the component values as arguments (or the default value for 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 <code>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.
import java.io.Serializable; public record EmployeeRecord ( Long id, String firstName, String lastName, String email, int age) implements Serializable { }
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class RecordExample { public static void main(String[] args) { EmployeeRecord e1 = new EmployeeRecord (1l, "Lokesh", "Gupta", "howtodoinjava@gmail.com", 38); writeToFile(e1, "employee1"); System.out.println(readFromFile("employee1")); } static void writeToFile(EmployeeRecord obj, String path) { try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(path))){ oos.writeObject(obj); } catch (IOException e) { e.printStackTrace(); } } static EmployeeRecord readFromFile(String path) { EmployeeRecord result = null; try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))){ result = (EmployeeRecord) ois.readObject(); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); } return result; } }
Program output:
EmployeeRecord[id=1, firstName=Lokesh, lastName=Gupta, email=howtodoinjava@gmail.com, age=38]
3.5. Additional fields and methods
Adding a new field and method is possible, but not recommended.
A new field added to record (not added in component list) must be static
. Adding a method is also possible which can access the internal state of record fields.
The added fields and methods are not used in implicitly generated byte code by 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 EmployeeRecord( Long id, String firstName, String lastName, String email, int age) implements Serializable { //additional field static boolean minor; //additional method public String fullName() { return firstName + " " + lastName; } }
3.6. Compact Constructor
We can add constructor specific code for data validation in compact constructors. It helps in building a record which is valid in given business context.
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 happen in canonical constructor in usual manner.
public record EmployeeRecord( Long id, String firstName, String lastName, String email, int age) implements Serializable { public EmployeeRecord { if(age < 18) { throw new IllegalArgumentException( "You cannot hire a minor person as employee"); } } }
4. API Changes
4.1. Class class
The Class class has two methods – isRecord()
and getRecordComponents()
. The getRecordComponents()
method returns an array of RecordComponent objects.
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.
4.2. ElementType enumeration
The ElementType enumeration has a new constant for records, RECORD_COMPONENT
.
4.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
.
5. Conclusion
Java records are an extremely useful feature and great addition to Java type system. It helps in cutting down boilerplate code written for simple data carrier classes almost completely.
But we should use it carefully and do not try to customize it 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.
Drop me your questions and suggestions related using records in Java 14
in comments.
Happy Learning !!
Read More : Infoq Article by Brian Goetz