Showing posts with label canonical constructor. Show all posts
Showing posts with label canonical constructor. Show all posts

Friday, February 19, 2021

Serialization of Record with circular references

There is a difference in the way records gets serialized and de-serialized. The complete spec for record serialization is available at https://docs.oracle.com/en/java/javase/15/docs/specs/records-serialization.html for reference.

Now lets consider one specific case where we have circular reference between objects that are serialized. 

Below code sample illustrates this

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Cat class. Contains a member of type Kitten
 */
class Cat implements Serializable {

    private Kitten child;

    // Constructor
    public Cat(Kitten child) {
        this.child = child;
    }

    // Accessor to get child reference
    public Kitten child() {
        return this.child;
    }

    public String toString() {
        return "Mother@"+this.hashCode();
    }
}

/**
 * Kitten class. Contains a member of type Cat
 */
class Kitten implements Serializable {

    private Cat mother;

    // Accessor to get mother reference
    public Cat mother() {
        return mother;
    }

    // Setter to set mother of the Kitten
    public void setMother(Cat mother) {
        this.mother = mother;
    }

    
    public String toString() {
        return "Child@"+this.hashCode();
    }

}

public class SerializeTest {
    public static void main(String args[]) throws IOException, FileNotFoundException, ClassNotFoundException {
        
        Kitten child = new Kitten(); // Creating a Kitten object
        Cat mother = new Cat(child); // Creating a Cat object, with Kitten object created as its child
        child.setMother(mother); // Set the Cat object created in the previous step as mother for the Kitten object, thus creating circular reference

        System.out.println("****Before****");
        print(mother); // Print the object tree of the mother

        // Serialize and write the mother object to a file
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("out.obj"));
        os.writeObject(mother);
        os.close();

        // De-serialize and read the mother object from the file
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("out.obj"));
        mother = (Cat) in.readObject();

        System.out.println("****After****");
        print(mother); // Print the object tree of the mother after de-serialization
    }

    // Utility method to print the mother object tree
    public static void print(Cat mother) {
        System.out.println("mother is: " + mother);
        System.out.println("mother.child is: " + mother.child());        
        System.out.println("mother.child.mother is: " + mother.child().mother());
    }
}

Here we have a Cat & Kitten classes. Cat has a reference to Kitten & Kitten has a reference to Cat. We create instances of Cat and Kitten and set references so as to create a circular reference. We then print the object tree to see the circular referencing of objects in the object tree. 

Now we write the object to a file thus serializing the object tree. We then read from the file and recreate the object tree thus performing de-serialization. Comments are added in the code snippet above providing details about the logic. Pls. refer the same for additional details.

Note that in this version of code Cat and Kitten are now regular classes. We will change the Cat implementation as a Record and see what happens shortly, but before that lets see how the serialization and deserialization of object tree behaves with regular classes.  

We execute the above code and get the output as shown below

****Before****
mother is: Mother@1325547227
mother.child is: Child@1528902577
mother.child.mother is: Mother@1325547227
****After****
mother is: Mother@584634336
mother.child is: Child@1469821799
mother.child.mother is: Mother@584634336

We see that circular references are preserved after de-serialization. Perfect!!! 

Now lets change the Cat implementation as Record. Below is the excact same code with Cat implementation alone changed as record class instead of a regular class

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * Cat class. Contains a member of type Kitten
 */
record Cat(Kitten child) implements Serializable {

    public String toString() {
        return "Mother@"+this.hashCode();
    }
}

/**
 * Kitten class. Contains a member of type Cat
 */
class Kitten implements Serializable {

    private Cat mother;

    // Accessor to get mother reference
    public Cat mother() {
        return mother;
    }

    // Setter to set mother of the Kitten
    public void setMother(Cat mother) {
        this.mother = mother;
    }

    
    public String toString() {
        return "Child@"+this.hashCode();
    }

}

public class SerializeTest {
    public static void main(String args[]) throws IOException, FileNotFoundException, ClassNotFoundException {
        
        Kitten child = new Kitten(); // Creating a Kitten object
        Cat mother = new Cat(child); // Creating a Cat object, with Kitten object created as its child
        child.setMother(mother); // Set the Cat object created in the previous step as mother for the Kitten object, thus creating circular reference

        System.out.println("****Before****");
        print(mother); // Print the object tree of the mother

        // Serialize and write the mother object to a file
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("out.obj"));
        os.writeObject(mother);
        os.close();

        // De-serialize and read the mother object from the file
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("out.obj"));
        mother = (Cat) in.readObject();

        System.out.println("****After****");
        print(mother); // Print the object tree of the mother after de-serialization
    }

    // Utility method to print the mother object tree
    public static void print(Cat mother) {
        System.out.println("mother is: " + mother);
        System.out.println("mother.child is: " + mother.child());        
        System.out.println("mother.child.mother is: " + mother.child().mother());
    }
}

See how succinct and void of ceremonies the Cat implementation is as Record...

Now we execute this code and the output is...

****Before****
mother is: Mother@764977973
mother.child is: Child@764977973
mother.child.mother is: Mother@764977973
****After****
mother is: Mother@1811075214
mother.child is: Child@1811075214
mother.child.mother is: null

Before image object tree is the same as that with the regular classes. 

But after serialization and de-serialization, we see that circular reference is broken and the default value for object - 'null' is set instead for the child objects mother reference. 

This is because of the way a Record objects gets de-serialized:

  1. First all the component fields of the Record gets created - reading from the stream data
  2. And then Record object is created by calling the Record class canonical constructor  

When the first step is performed in our example, Kitten (child) object gets created and since at this time the Cat (mother) record object is not yet created, it set to the default null value. 

Then in the second step, Cat (mother) object is created with the Kitten (child) object created in the 1st step as its component. This Kitten (child) object has its mother set to null which explains the output above. 

For full technical details on serialization & de-serialization of record classes, see the spec link mentioned at the top of this post 


Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Serialization-of-Record-with-circular-references

Compact constructor for Record classes

Consider a scenario where we wanted to validate the parameter before constructing the object. 

In our Cat record class example, we wanted to validate the color parameter before allowed an instance of Cat to be created. 

We can have a canonical constructor for our Cat record, in which we include the validation. Code for this shown below

record Cat(String color) {
    Cat(String color) {
        if (color.matches(".*\\d.*")) {
            throw new IllegalArgumentException("Invalid color");
        }
        this.color = color;
    }
}

Here, we validate the color parameter in the constructor to check if it contains any numeric character in it. 

And if it does, we throw an IllegalArgumentException. Otherwise we proceed to complete the creation of the object by assigning the color to auto-generated instance variable.

Note however that there is repetition of the parameter list - one at the record header and again the same list is repeated in the constructor. 

This can be avoided by using a compact form of constructor declaration supported for record classes. 

Below is the same code with compact constructor

record Cat(String color) {
    Cat {
        if (color.matches("*\\d*")) {
            throw new IllegalArgumentException("Invalid color");
        }
    }
}

The parameter list for compact constructor is implicit, and is the same as that of component list in record definition. 

Also note the missing assignment statement in the compact constructor form

this.color = color;

This again is implicit, all implicit parameters gets assigned to their corresponding instance variables. 

Another question is at what point in time is this assignment done? 

Well, its done at the end of the constructor, after all the custom validation code in the compact constructor is executed. To illustrate this, consider the following code 

record Cat(String color) {
    Cat {
        if (this.color.matches("*\\d*")) {
            throw new IllegalArgumentException("Invalid color");
        }
    }
}

Here we try to use this.color in our validation logic. 

This code fails to compile, throwing an error "variable color might not have been initialized", indicating that the parameter assignment to instance variables happens at the end of construtor.


Sample code used in this post can be downloaded from https://github.com/ashokkumarta/awesomely-java/tree/main/2021/02/Language-Features/Record/Compact-constructor