Exploring the Power of Java Records: Simplifying Data Classes and Enhancing Code Readability

Java Records ( JEP-395 ), introduced as a preview feature in JDK 14 and officially released in JDK 16, is a new feature that simplifies the creation of simple data carrier classes. Gone are the days of writing verbose boilerplate code for these classes; Java Records provides a compact syntax to declare these classes with ease. In this blog post, we’ll explore what Java Records are, why they’re important, and how to use them with examples.

What Are Java Records?

Java Records is a new type of class introduced to combat boilerplate code, which is repetitive and uninteresting but necessary for data carrier classes.

Advantages of Java Records

1. Reduces Boilerplate Code: Java Records reduces the amount of extra code needed for data carrier classes.

2. Immutability: Java Records automatically creates immutable classes by default.

3. Better Encapsulation: By using records instead of regular classes for simple data containers, developers can focus on the important aspects of their application’s logic and design.

4.Nominal Tuple: Records data type loosely represents the tuple data structure.

As developers, we often create Java bean classes with just setters, getters, and standard methods like `equals()`, `hashCode()`, and `toString()`. like below

public class Employee {

    private int id;
    private String name;

    private String address;

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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


    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return id == employee.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }


}
Code language: Java (java)

Java Records removes the necessity to write all this code by providing a concise way of declaring data carrier classes.

Creating Java Records

To create a record, simply use the `record` keyword followed by the class name and the list of instance variables enclosed within parentheses. Let’s dive into some examples.

The Employee class can be replaced with following code.

public record Employee ( int id, String name, String address) {
}Code language: Java (java)

If you decompile the record class with javap command,

javap target/classes/dev/fullstackcode/record/example/Employee.classCode language: Java (java)

you can see structure like below.

public final class dev.fullstackcode.record.example.Employee extends java.lang.Record {
  public dev.fullstackcode.record.example.Employee(int, java.lang.String, java.lang.String);
  public final java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public int id();
  public java.lang.String name();
  public java.lang.String address();
}
Code language: Java (java)

You can observe following things in generated class file.

  • Generates a public accessor method.
  • Generates a canonical constructor
  • Generates equals and hashcode
  • Generates toString() method with all the fields.
  • Record classes extends java.lang.Record
  • Record classes are final
  • There is no default constructor.

Working with Java Records

Accessing record components is just like accessing fields in standard classes, using the dot (.) operator. A quick example:

public record Employee(int id, String name, String address) {

}


class Main {

    public static void main(String[] args){
        Employee emp = new Employee(1,"John","USA");
        System.out.println("Employee name :" + emp.name());
    }
}
Code language: Java (java)

Customizing Java Records

Java Record provides some freedom to customize them.

Explicit accessor methods

If you want to customize the return value of accessor method, you can explicitly write accessor method and customize the return value.

public record Employee(int id, String name, String address) {

    @Override
    public String name() {
        return name.toUpperCase(Locale.ROOT);
    }
}


class Main {

    public static void main(String[] args){
        Employee emp = new Employee(1,"John","USA");
        System.out.println("Employee name : " + emp.name());

    }
}
Code language: Java (java)

The program will print Employee name in CAPITAL letters.

Employee name :JOHNCode language: Java (java)

Canonical Constructor

Record lets you override the generated canonical constructor. This is really useful when you want to implement some validations on fields before creating the object.

Let’s say you want to create a employee object but they are few validations need to perform on the fields before creating employee object.

public record Employee(int id, String name, String address) {

    public Employee(int id, String name, String address) {
        Objects.requireNonNull(name);
        if(id <= 0) {
            throw new IllegalArgumentException("id should be greater than zero");
        }
        this.id = id;
        this.name = name;
        this.address = address;
    }

    @Override
    public String name() {
        return name.toUpperCase(Locale.ROOT);
    }
}


class Main {

    public static void main(String[] args){
     Employee emp = new Employee(1,null,"USA");
     System.out.println("Employee name : " + emp.name());      

    }
}
Code language: Java (java)

If you run above program you will get following error

Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:208)
	at dev.fullstackcode.record.example.Employee.<init>(Employee.java:9)
	at dev.fullstackcode.record.example.Main.main(Employee.java:31)Code language: Java (java)

Compact Constructor

If you look at the overriding canonical constructor example, the constructor has lot of ceremonial/boilerplate code. We can make constructor more compact by removing boiler plate code.

The above example can be re-written like below. Please observe that we removed parameters of constructor and assignments inside constructor. We are just left with the required validations code.

public record Employee(int id, String name, String address) {

    public Employee {
        Objects.requireNonNull(name);
        if(id <= 0) {
            throw new IllegalArgumentException("id should be greater than zero");
        }       

    }
    @Override
    public String name() {
        return name.toUpperCase(Locale.ROOT);
    }
}


class Main {

    public static void main(String[] args){

        Employee emp = new Employee(0,"John","USA");
        System.out.println("Employee name : " + emp.name());

    }

}
Code language: Java (java)

Custom Methods

Apart from explicit accessor methods, record let’s you write custom getter methods. Custom methods are useful when you want to write any utility functions.

In below example, to get full name of employee, we can just implement a method to combine both first and last name.

public record Employee(int id, String firstName,String lastName, String address) {

    @Override
    public String firstName() {
        return firstName.toUpperCase(Locale.ROOT);
    }

    public String getFullName() {
        return firstName + " "+ lastName;
    }
}


class Main {

    public static void main(String[] args) {

        Employee emp = new Employee(0,"John","Smith", "USA");
        System.out.println("Employee name : " + emp.getFullName());

    }
}Code language: Java (java)

Implementing interfaces

Earlier in the post, we have seen that Record class are final. So you can not extend the other classes but you can implement the Interfaces.

Below code example shows the Record implementing “Comparable” interface to sort the Employee records by first and last name;

public record Employee(int id, String firstName, String lastName,
                       String address) implements Comparable<Employee>, Serializable {

    @Override
    public String firstName() {
        return firstName.toUpperCase(Locale.ROOT);
    }

    public String getFullName() {
        return firstName + " " + lastName;
    }


    @Override
    public int compareTo(Employee o) {

        int compare = this.firstName.compareTo(o.firstName);
        if (compare == 0) {
            return this.lastName.compareTo(o.lastName);
        }
        return compare;
    }
}


class Main {

    public static void main(String[] args) {

        List<Employee> empList = new ArrayList<>();

        empList.add(new Employee(1, "John", "Smith", "USA"));
        empList.add(new Employee(1, "John", "Wright", "USA"));
        empList.add(new Employee(1, "Adam", "Nell", "USA"));

        Collections.sort(empList);

        System.out.println(empList);

    }
Code language: Java (java)
[[Employee[id=1, firstName=Adam, lastName=Nell, address=USA], 
Employee[id=1, firstName=John, lastName=Smith, address=USA], 
Employee[id=1, firstName=John, lastName=Wright, address=USA]]
Code language: Java (java)

Improved Serialization

Another main advantage of record data type is improved serialization process in Java.

In Java serialization process, any class that works with the java.io.Serializable interface can be serialized – quite simple!

This interface doesn’t have any members; its only purpose is to mark a class for serialization. During the serialization, the state of all non-transient fields (even private ones) is gathered and added to the serial byte stream. When deserializing, a superclass’ no-arg constructor creates an object whose fields are then filled with the state taken from the serial byte stream. The serial byte stream’s format (the “serialized form”) is determined by Java Serialization unless you use writeObject and readObject methods for custom formatting.

Java Serialization has its issues, as Brian Goetz highlights in Towards Better Serialization. The main problem is that it wasn’t designed to be a part of Java’s object model. This means that Java Serialization utilizes objects with less conventional methods like reflection, rather than using an object class’s API. For instance, it’s possible to create a new deserialized object without calling one of its constructors, and data from the serial byte stream isn’t checked against constructor specifications.

Record Serialization

In Java Serialization, a record class is made serializable just like a normal class, by implementing java.io.Serializable:

 Java Serialization treats a record very differently than an instance of a normal class.

The design is based on two properties that attempt to keep things as simple as possible.

1.Serialization of a record is based only on its state components

2.Deserialization uses only the canonical constructor.

Unlike classes no customized serialization is allowed for records. As record is immutable it can only ever have one state. so there is no need to allow customization of the serialized form.

 On the deserialization side, the only way to create a record is through the canonical constructor of its record class, whose parameters are known because they are identical to the state description

For normal classes, Java Serialization relies heavily on reflection to set the private state of a newly deserialized object. However, record classes expose their state and means of reconstruction through a well-specified public API, which Java Serialization leverages.

Java Record vs Traditional Class: When to Choose Which?

The canonical constructor of a record class has the same signature as the state, as opposed to the no-arg default constructor introduced to regular classes.

If an object requires mutable state or state that is unknown when the object is formed, a record class should not be used; instead, a standard class should be specified.

Similar Posts