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:
- First all the component fields of the Record gets created - reading from the stream data
- 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