The Java programming language is often criticized for being overly verbose. A common example is that creating a simple POJO class requires writing many lines of code. However, since Java 14, this criticism has been addressed by introducing a new type called the record class. In this tutorial, we will explore how to use record classes effectively.

The acronym POJO (Plain Old Java Object) is common in the
Java community. The POJO is used to exchange data between programs. Traditional POJO looks like this:

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public class Customer {

private long id;

private String fullName;

private String phoneNumber;

private String address;

private LocalDate createdAt;

}

This POJO class holds data about a customer. To create an object from this class and initialize all fields, we need to define a constructor. Since all fields are private, we must add getters and setters to access and modify each field individually. It is also a good practice to override the toString, hashCode, and equals methods inherited from the Object class. So now our class will look like this:

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;
import java.util.Objects;

public class Customer {

private long id;

private String fullName;

private String phoneNumber;

private String address;

private LocalDate createdAt;

public Customer(long id, String fullName, String phoneNumber, String address, LocalDate createdAt) {
this.id = id;
this.fullName = fullName;
this.phoneNumber = phoneNumber;
this.address = address;
this.createdAt = createdAt;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public LocalDate getCreatedAt() {
return createdAt;
}

public void setCreatedAt(LocalDate createdAt) {
this.createdAt = createdAt;
}

public String getFullName() {
return fullName;
}

public void setFullName(String fullName) {
this.fullName = fullName;
}

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getPhoneNumber() {
return phoneNumber;
}

public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Customer customer = (Customer) o;
return Objects.equals(id, customer.id) && Objects.equals(fullName, customer.fullName)
&& Objects.equals(phoneNumber, customer.phoneNumber) && Objects.equals(address, customer.address)
&& Objects.equals(createdAt, customer.createdAt);
}

@Override
public int hashCode() {
return Objects.hash(id, fullName, phoneNumber, address, createdAt);
}

@Override
public String toString() {
return “Customer{” +
“address='” + address + ”’ +
“, id='” + id + ”’ +
“, fullName='” + fullName + ”’ +
“, phoneNumber='” + phoneNumber + ”’ +
“, createdAt=” + createdAt +
‘}’;
}
}

Our class has grown from 18 lines to a staggering 96 lines, essentially increasing its size fivefold with boilerplate code. This pattern is repeated for all our POJO classes. To address this issue, many Java developers have turned to the Lombok library. Recognizing the problem, the designers of the Java programming language introduced a new type of class called a record, specifically designed to hold data.

Record classes are a special kind of class that helps model plain data aggregates with less ceremony than normal classes.

The record class has the following syntax:

accessModifier record name(commaSeparatedComponents) // record header
{ } // record body

There are a few restrictions applied to records:

A record is implicitly final, meaning it cannot be extended and cannot contain any abstract methods.A record cannot have an explicit extends clause.The name of a record’s component cannot be the same as any methods declared in the Object class, except for equals.

Now, we can declare our class using a record.

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record CustomerRecord(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {
}

Now, we don’t have any boilerplate code, and the record declaration is even shorter than an initial class declaration. A compiler does all the rest.

Any record class implicitly extends the java.lang.Record abstract class. This class defines methods that override equals(), toString(), and hashCode() from the Object class. Consequently, all record classes inherit these methods, which can be either explicitly defined or implicitly provided by the compiler.

The compiler performs the following tasks for Java records:

Implicit Canonical Constructor: Creates a public, all-args constructor for the record.Field Declaration: Declares all fields as private and final, making them immutable. However, if a field references a mutable object, the object can be modified, but the reference itself cannot. Thus, records are considered shallowly immutable.Accessor Methods: Creates accessor methods for each field without the “get” prefix; the method names match the field names. No setter methods are created since all fields are immutable.equals Method: Overrides the equals method from the Object class. This method compares the component fields in the order they are specified and returns true if all corresponding fields match.hashCode Method: Overrides the hashCode method from the Object class. It calculates a hash code based on the values of the component fields in the specified order.toString Method: Overrides the toString method from the Object class. It returns a string representation of the record, including the record name and the name-value pairs of each component field in the specified order.

Instance methods

Instance methods can be declared.

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record InstanceMethod(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {

public void printCreatedAt() {
System.out.println(“createdAt = ” + createdAt);
}

@Override
public long id() {
return 1000000L + id;
}

public long id(long prefix) {
return prefix + id;
}

// won’t compile
// public String id() {
// return “prefix” + id;
// }
}

In Java records, methods corresponding to a member name can be redeclared/overridden, but they must adhere to the following rules:

They must have a public access modifier.They cannot have any parameters.They cannot throw any exceptions.They must have the same return type as the field they correspond to.They cannot be generic.

While we can have instance methods in a record, we cannot add any additional instance fields or instance field initializations.

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record InstanceFields(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {
// private String instanceField;

// {
// id = 1L;
// }
}

Static members

Static variables, static initializers, and static methods can be declared as well:

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record StaticMembers(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {

private static final String STATIC_STRING;

static {
STATIC_STRING = “static string 1”;
}

private static final String STATIC_STRING_2 = “static string 2”;

public static String getStaticString() {
return STATIC_STRING_2;
}

public enum RecordEnum {
YES, NO
}
}

Even enums can be declared inside a record.

Implement interface

The record can’t explicitly extend any class. However, it can implement any interface. Let’s say we have the following interface:

package com.polovyi.ivan.tutorials;

public interface User {

String getUsername();

}

Now, the Record can extend it:

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record ImplementsInterface(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) implements User {

@Override
public String getUsername() {
return “UserName”;
}
}

Generic record

We can declare a record using generic types.

package com.polovyi.ivan.tutorials;

public record GenericRecord<T>(T parameter) {

T getParameter() {
return parameter;
}
}

Now, we can use it like this:

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public class Examples {

public static void main(String[] args) {
GenericRecord<String> genericRecord = new GenericRecord<>(“GenericRecord”);
String strParameter = genericRecord.getParameter();
System.out.println(“strParameter = ” + strParameter);

GenericRecord<Long> genericRecord2 = new GenericRecord<>(1L);
Long longParameter = genericRecord2.getParameter();
System.out.println(“longParameter = ” + longParameter);
}
}

Constructors

The most confusing part about records is the constructor. There is an implicit constructor created by the compiler, known as the canonical constructor. This constructor assigns each argument from the new expression that instantiates the record class to the corresponding component field.

Explicit canonical constructor

We can explicitly specify the canonical constructor in a record, for example, to add additional features such as field validation. This explicit constructor functions like a constructor in a traditional class. Note that defining an explicit canonical constructor will override the implicit canonical constructor provided by the compiler.

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record ExplicitCanonicalConstructor(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {

public ExplicitCanonicalConstructor(long id, String fullName, String phoneNumber,
String address, LocalDate createdAt) {
System.out.println(“Inside explicit canonical constructor.”);
if (createdAt == null) {
createdAt = LocalDate.now();
}
this.id = id;
this.fullName = fullName;
this.phoneNumber = phoneNumber;
this.address = address;
this.createdAt = createdAt;
}
}

There are a few rules to follow when declaring an explicit canonical constructor in a record:

The signature must match the implicit canonical constructor, with the parameter list exactly matching the record’s header.The constructor’s accessibility cannot be more restrictive than the record’s accessibility. For example, if the record is protected, the constructor can be protected or public and cannot be private .It cannot invoke other constructors using this().The constructor cannot be generic.The constructor cannot use a throws clause. All checked exceptions must be handled within the constructor.

Compact canonical constructor

The compact canonical constructor is a special type of canonical constructor that does not explicitly list parameters or initialize fields within its body. Instead, the parameter list is derived from the record header, and the initialization is implicitly handled by the canonical constructor. When using the compact constructor, the argument values are passed similarly to any other constructor.

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public record CompactCanonicalConstructor(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {
public CompactCanonicalConstructor {
// won’t compile
// this(1l, “FullName”, “+17737278341”, “address”, LocalDate.now());
if (createdAt == null) {
createdAt = LocalDate.now();
}
System.out.println(“Inside Compact canonical constructor.”);
}
}

The compact canonical constructor is invoked first, followed by the implicit or explicit canonical constructor, depending on which is present. It is often used for parameter validation and has access to all fields and methods of the record.

The compact constructor can be identified by its special syntax, which lacks parentheses in the constructor header.

Despite not explicitly listing parameters, the record does not have a no-argument constructor. When creating an object from the record, all parameters must still be passed to the constructor. The difference is that the compact constructor’s code is executed first, followed by the implicit canonical constructor.

package com.polovyi.ivan.tutorials;

import java.time.LocalDate;

public class Examples {

public static void main(String[] args) {
CustomerRecord customerRecord = new CustomerRecord(1l, “FullName”, “+17737278341”, “address”, LocalDate.now());
CompactCanonicalConstructor compactCanonicalConstructor = new CompactCanonicalConstructor(1l,
“FullName”,
“+17737278341”,
“address”,
null);
// won’t compile
// CompactCanonicalConstructor crCompactCanonicalConstructor2 = new CompactCanonicalConstructor();

}
}

All restrictions for explicit canonical constructors apply to the compact canonical constructor.

When using Java records, you can either specify an explicit canonical constructor or a compact canonical constructor, but not both for the same record. This restriction ensures that there is no ambiguity in how the record is initialized.

Non-canonical record constructor

A non-canonical record constructor is a constructor that has a signature different from the canonical constructor. A record class can specify any number of non-canonical constructors. However, a non-canonical constructor must delegate to another constructor (either a canonical or another non-canonical constructor) using the this keyword. If the delegation call to the canonical constructor is not specified using this, the compiler will report an error.

package com.polovyi.ivan.tutorials;

import java.security.InvalidParameterException;
import java.time.LocalDate;

public record NonCanonicalConstructor(long id,
String fullName,
String phoneNumber,
String address,
LocalDate createdAt) {

public NonCanonicalConstructor() throws Exception{
this(1l, “FullName”, “+17737278341”, “address”, LocalDate.now());
System.out.println(“Inside no-arg non canonical constructor”);
}

protected NonCanonicalConstructor(long id) throws InvalidParameterException {
this(id, “FullName”, “+17737278341”, “address”, LocalDate.now());
System.out.println(“Inside non canonical constructor”);
}

// won’t compile
// public NonCanonicalConstructor(long id, String fullName) {}
}

Remember that the chaining of constructors must ultimately lead to the canonical constructor call so that all fields are initialized.

Constructors, for example, can have an exception declaration and be less accessible than the record itself. They are mainly used for the creation of specialized objects.

The complete code can be found here:

GitHub – polovyivan/java-records

Conclusion

The introduction of records to the Java language has been eagerly anticipated. They streamline the creation of data objects, offering an efficient memory footprint and runtime performance. Their immutability also ensures thread safety in concurrent applications. Generally, a record class focuses on providing easy access to the data rather than performing complex data processing.

Thank you for reading! If you enjoyed this post, please like and follow it. If you have any questions or suggestions, feel free to leave a comment or connect with me on my LinkedIn account.

Java Records was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.