Java 14 – record type

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:

  1. A constructor accepting all fields.
  2. The toString() method which prints the state/values of all fields in the record.
  3. The equals() and hashCode() methods using an invokedynamic based mechanism.
  4. The getter methods whose names are similar to field names i.e. id(), firstName(), lastName(), email() and age().
  5. The class extends java.lang.Record, which is the base class for all records. It means a record cannot extend other classes.
  6. The class is marked final, which means we cannot create a subclass of it.
  7. 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 &amp;lt; 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

Was this post helpful?

Join 7000+ Fellow Programmers

Subscribe to get new post notifications, industry updates, best practices, and much more. Directly into your inbox, for free.

Leave a Comment

HowToDoInJava

A blog about Java and its related technologies, the best practices, algorithms, interview questions, scripting languages, and Python.