Java Records Met Serialization

Introduction

A record Class is a plain aggregate of data; there is less ceremony in the declaration of a record class than that of a regular class. Records are all about data, the whole data and nothing but the data. It’s a transparent carrier of data so it makes its data available to its clients. 

There is a new syntax in the java language to declare a record class. A Record class tightly couples its API to its internal representation. The declaration of a record class is significantly more concise than that of a normal class. 

Before going forward in record with serialization we should know why we need record classes. To understand what is a record class & its implementation in the basic language you should go through the previous article here.

What are Records with its implementation

Here we will be designing a Java class named Location and will be using it later in this article. Designing a Location class with an aggregate of the city and country. We might use the Location class in a HashSet or a key in a hashmap. So we need to provide hashcode() and equals() implementations as well as toString() implementation.

import java.util.Objects;
public class Location {
    private String city;
    private String country;
    public Location(String city, String country) {
        this.city = city;
        this.country = country;
    }
    public String getCity() {
        return city;
    }
    public String getCountry() {
        return country;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Location location = (Location) o;
        return Objects.equals(city, location.city) && Objects.equals(country, location.country);
    }
    @Override
    public int hashCode() {
        return Objects.hash(city, country);
    }
    @Override
    public String toString() {
        return "Location{" + "city='" + city + '\'' + ", country='" + country + '\'' + '}';
    }
    public static void main(String[] args) {
        Location location = new Location("Sydney", "Australia");
        System.out.println(location);
    }
}

Output from the above code will be the Location object in the form of a JSON object. (Refer to the Screenshot below).

Location[city=Sydney, country=Australia]

So in the above program or code that we have written it does the obvious thing that two locations are considered equals if their cities are equal and their country is equal. Hashcode is computed from the city and country and additionally, the toString() implementation contains the city and country as well. 

Fundamentally location class is the aggregate of city and country and that is exactly what records give to us. Let’s convert and rewrite this location class as a record class.

There is a new keyword ‘record’ for declaring records immediately following the record keyword. We have the name of the record class, "Location, " and then in parenthesis, we have the record header which contains the components of the record and that we are done. 

public record Location(String city, String country) {
    public static void main(String[] args) {
        var loc = new Location("Sydney", "Australia");
        System.out.println(loc);
    }
}

Output from the above code will be the Location object in the form of a JSON object. (Refer to the Screenshot below).

Location[city=Sydney, country=Australia]

What do Records provide us?

Here we can see the string representation of the record being printed out. There are some more things records give us which are;

  • Public accessor methods
  • A Canonical constructor 
  • Reflexive Equality - equals and hashcode
  • String representation using all components - toString
  • Record classes are final

The compiler generates the public accessor methods for us, for a record class it will generate a canonical constructor and it will generate the hashcode, equals and toString implementations. Record classes are always final, if we look at the decompiled output of the record class Location, we use the command;

java  -p out/production/file_path

What Records doesn’t provide us?

Here, what record classes don’t provide us or we need to get customized? So here are some of that things;

  • Accessor for each component
  • Canonical constructor
  • Compact constructor
  • Can provide custom implementation methods
  • Can implementation interfaces 

We can provide an explicit accessor for each of the components, and we can provide an explicit constructor. Records also have a compact version of their constructor or record class is a restricted class, it is just a class it can’t have additional implementational methods and it can also implement interfaces. 

Let us explain a few of the above-mentioned customized things records provide;

Explicit Constructor 

So let us take the above program example only and consider a scenario if we want to provide an explicit constructor for the city or country method and this accessor is always going to normalize the value of the component. 

import java.util.Locale;
public record LocationNew(String city, String country) {
    public String city() {
        return this.city.toUpperCase(Locale.ROOT);
    }
    public String country() {
        return this.country.toUpperCase(Locale.ROOT);
    }
    public static void main(String[] args) {
        var loc = new LocationNew("Sydney", "Australia");
        System.out.println(loc.city());
        System.out.println(loc.country());
    }
}

So the output of the above program will return city and country name while executing the program.

Custom Implementation Methods 

Now if we look to create a custom implementation method, like for an example we can reverse the name of the city.

public record LocationNew(String city, String country) {
    public String reverseCity() {
        return new StringBuilder(this.city).reverse().toString();
    }
    public static void main(String[] args) {
        var loc = new LocationNew("Sydney", "Australia");
        System.out.println(loc.reverseCity());
    }
}

Let’s run the program and we will see the city name in reverse order.

Implement Interfaces 

Record classes can also implement interfaces. Let’s suppose we have an interface called City;

import java.io.Serializable;
interface City{ String city(); }
public record LocationNew(String city, String country)
implements City, Serializable, Comparable {
  @Override
  public int compareTo(Object o) {
      return 0;
  }
}
  class Main {
  public static void main(String[] args) {
      var loc = new LocationNew("Sydney", "Australia");     
  }
}

Key Points on Records

Here are some Key Points we should be aware of records in java are listed below;

  • Model data as data.
  • Easy to declare.
  • Boilerplate takes care of itself.
  • API is derived mechanically and completely from state description.
  • API includes protocols for construction, member access, equality and display.

Record in Java Serialization

 Serialization Steps

The concise design of records allowed to rethink the serialization protocol so far the records protocol is based on two properties,

  1. Serialization of a record is based on its state components so the serialized form is based on components and can’t be customized.
  2. Deserialization uses only the canonical constructor, this constructor is never bypassed and is the only constructor that is called during object creation.

The simplicity of this kind of protocol naturally flows from semantic constraints of records and they show how serialization is now a proper part of the object for records.

Deserialization Steps

This differs from normal classes, the difference is that for the normal classes we construct the object graph from Top to Down, but for records we do from Bottom to Up.

  1. Stream field values read and reconstruct, while reading the values from the stream means that we don’t create an object first we read the values and reconstruct them, so either reconstruct an object or hold primitives in memory.
  2. Values are matched against the matched components, we match those values against the record components that match by name and value and any values that don’t match a record component are dropped.
  3. Construction through canonical constructor i.e component values as arguments, we call the canonical constructor with the values as arguments.

Summary

To finish, let us sum up how records make serialization better. Below are mentioned some points,

  • The design of records fits the demand of serialization, design of records really naturally fits the demands of serialization, they are data oriented classes which are naturally suitable but also they have very restricted extensibility and final state.
  • It’s much easier to handle records during the serialization process. So the semantic constraints they have allows tightening of the serialization protocol.
  • The serialized form is known and can be trusted, serialized form is known and trusted; it's always the record state there’s no customization allowed; it's much easier to maintain.
  • State always accessible via component accessors, we can always use the accessors to retrieve record state, it’s more secure.
  • Object creation only through canonical constructor, object creation is only allowed through canonical constructor.


Similar Articles