Java Serialization

This article will explore the concept of serialization in java, its use, advantages and how to perform it with examples.
It will also cover advanced concepts such as skipping some fields from serialization, behavior in case of super classes and static fields.

What is serialization
Serialization is the process or mechanism to convert an object to a byte stream, which can be saved to a file, sent over the network or saved in memory.
Saving an object means saving the values of its variables so that when it is read back(deserialized), we get the same object.
Serialization in java
Deserialization is the reverse of serialization where saved or serialized object is restored back.
Deserialization in java

Serialization uses
Following are the areas where serialization might be useful.
1. Saving current state of a game or characters of a game.
2. In drawing applications, where you draw a certain design up to a point and save it.  It is serialized as an object and deserialized when loaded again.
3. Sending java objects over the network as json(Serialization) and creating objects from json(Deserialization) in REST apis.
Serialization benefits
Imagine if you were to save the state of an object manually. You need to write the values of all its variables manually during serialization and read them back during deserialization in the correct order.
What if an object contains a reference to another object, which, in turn references another object. It would be a nightmare.

Java serialization process takes care of all this stuff automatically, which is its biggest benefit.

How it works
Java provides two classes which take care of serialization and deserialization.

java.io.ObjectOutputStream
This class is responsible for saving the state of an object or serializing the object.
It takes an OutputStream argument. This output stream might be pointing to a file or a network stream.
ObjectOutputStream writes the object to this stream using its writeObject() method which takes the object as argument.

java.io.ObjectInputStream
This class is responsible for reading serialized object and create an exact same object out of it.
It takes an InputStream argument. This input stream might be pointing to a file or a network stream.
ObjectInputStream reads the object from this stream using its readObject() method. It does not take any argument.
Serialization example
Consider below class whose object we will be serializing or saving to a file and reading back or deserializing.

package com.codippa;

public class Laptop {
  private String brand;
  private String model;
  private float size;
  
  public Laptop(String b, String m, float s) {
    this.brand = b;
    this.model = m;
    this.size = s;
  }
  // getter and setter methods
}

Below is the code for serialization and deserialization

public class SerializationDemo {

  public static void main(String[] args) {
    Laptop ls = new Laptop("Dell", "XYZ", 12.5);
    System.out.println("Object before saving: " + ld);
    try {
      ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("d:\\laptop.ser"));
      // save object to file
      oos.writeObject(ls);
      oos.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
    try {
      ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("d:\\laptop.ser"));
      // read object from file
      Laptop ld = (Laptop)ois.readObject();
      ois.close();
      System.out.println("Laptop brand is: " + ld.getBrand());
      System.out.println("Laptop model is: " + ld.getModel());
      System.out.println("Laptop size is: " + ld.getSize());
      System.out.println("Object after reading: " + ld);
    } catch (IOException e) {
      e.printStackTrace();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
}

Above code creates an object and writes it to a file with writeObject() method of ObjectOutputStream.
Then, it reads back the object from the file using readObject() of ObjectInputStream and prints the fields of object.
Output is

Object before saving: com.codippa.Laptop@7de26db8
Laptop brand is: Lenovo
Laptop model is: XYZ
Laptop size is: 12.5
Object after reading: com.codippa.Laptop@7de26db8

Hashcode of the saved object and the one read back shows that both are same. We also get the same values of instance variables that were saved.

Following are some of the points worth remembering regarding java serialization:

1. Objects of a class are serialized. The class itself is not serialized.
2. File extension where serialized object is saved need not be “.ser”. It is just used to differentiate serialized object files with other files.
3. Class of the serialized object must implement Serializable interface. It is a marker interface, meaning, it has no methods.
If the class does not implement Serializable, then you will get

java.io.NotSerializableException: com.codippa.Laptop

4. Return type of readObject() is java.lang.Object. You need to cast it to the type of class of serialized object.
5. This is the reason that readObject() throws a ClassCastException.

SerialVersionUID
When an object is serialized, java calculates and assigns a version number to its class internally. This number is called SerialVersionUID.

At the time, it is deserialized back, again a version is calculated and compared with that the serialized version to ensure that the class has not changed after the object was serialized.
It will allow deserialization only when both versions match.

So, suppose you serialize an object of a class, then add a field to it. Now, trying to create the object back or deserializing, will raise below error

java.io.InvalidClassException: com.codippa.Laptop; local class incompatible: stream classdesc serialVersionUID = -7664685812937925942, local class serialVersionUID = 8100668448402885909

To prevent java serialization mechanism from generating SerialVersionUID, define your own id with the below syntax

private static final long serialVersionUID = 423432423425788L;

Name is significant and its type needs to be long. It is declared static because it is shared by all objects.
Serialization with nested objects
Till now, all the class variables were built in types such as primitives and objects. But, what if a class HAS A object of another type, such as below.

public class Laptop implements Serializable {
  private String brand;
  private Spec spec;
}

public class Spec {
  private int ram;
  private int hdd;
  // getter and setter
}

What will be the serialization behavior in this case?

A nested object will also be serialized if its class implements Serializable. If not, then serializing outer object will throw a NotSerializableException.

If the nested object contains another nested object and it does not implement Serializable, then also a NotSerializableException will be thrown.

So, for an object that contains a custom java object, it can only be serialized if the entire hierarchy implements Serializable.
If any one nested object does not implement it, serialization will fail.
Serialization with transient fields
If an object contains another object and it does not implement Serializable, then you can not serialize it.
In above example, if Spec does not implement Serializable, then Laptop object can not be serialized.

There is one way to solve this. Modify Spec to implement Serializable. But, what if
1. You do not have access to Spec source code.
2. You are not allowed to modify Spec class.
3. Spec contains another object and you can not change it.

In any of the above cases, serializing Laptop object will result in NotSerializableException.

There is one solution to this.
Make spec field as transient.  When a field is marked as transient, it is skipped from being serialized and its default value is saved.
So, for objects, null will be saved. For boolean values, false; for int values, 0 and so on.

Custom Serialization
Consider the problem stated in last section. We have a nested object which does not implement Serializable.
So, to save the outer object, we need to make the nested field as transient. When the outer object is deserialized, the nested field will be null.
What if we want a complete object after deserialization. That is, when Laptop object is deserialized, its spec field should be null but contains a working object.

Java serialization mechanism provides following two methods to tweak the process, where in we can add our own logic during serialization and deserialization. They are

private void readObject(ObjectInputStream ois): Called during serialization.

private void writeObject(ObjectOutputStream oos): Called during deserialization.

These methods are defined the class whose object we are serializing. The idea is to create a spec object at the time of deserialization using below steps

A. Save the field values of spec object in writeObject() using writeInt() method of ObjectOutputStream(since both spec values are int) during serialization.
B. Read them back in readObject() method using readInt() method.
C. Create a new Spec object, assign these values to its fields and initialize spec field of Laptop object with the newly created object.

Example,

public class Laptop implements Serializable{
 
  private String brand;
  private String model;
  private transient Spec spec;

  private void readObject(ObjectInputStream ois) {
    // create new object
    spec = new Spec();
    try {
      // set its fields
      spec.setRam(ois.readInt());
      spec.setHdd(ois.readInt());
      // read remaining object
      ois.defaultReadObject();
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
  
  private void writeObject(ObjectOutputStream oos) {
    try {
      // write field values for spec object
      oos.writeInt(8);
      oos.writeInt(512);
      // write remaining object
      oos.defaultWriteObject();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Remember that you need to call defaultWriteObject() and defaultReadObject() from writeObject() and readObject() for normal serialization and deserialization process to take place.

Also, the order in which values are read, should be the same in which they are written.
Inheritance in Serialization
If a class implements Serializable, then all its child classes are also serializable. In such case, a class can be normally serialized and deserialized.

Suppose a class implements Serializable but its parent class does not, then what should be the behavior.
In this case, the fields that are part of the object itself are serialized and deserialized normally but the fields that are inherited by the class or which belong to the parent class are not serialized.
They are set to their default values after deserialization. Example,

public class Device {
  private String type;
  
  public Device() {
    type = "unknown"; 
  // getter and setter
}

public class Laptop extends Device implements Serializable {
  private String brand; 
  private String model;

  // getter and setter
}

In this case when Laptop object is deserialized, its type field is set to “Unknown”.
The reason is that at the time of deserialization, the constructor of first non-serializable super class will run.
If there was no constructor in Device class, then type would be set to null.

Remember that the constructor will not run if the parent class(or parent of parent) implements Serializable.
Serialization with static
Only instance variables of a class are serialized. static variables are not serialized.

Reason could be understood by below explanation.
Suppose at the time of serializing an object, the value of static field was different and at the time of serializing another object, its value was different.
Then, at the time of deserialization, what should be the value of static field. It can not be different for different objects since, static variables are shared by all objects.

To avoid this confusion, java serialization does not save static fields. At the time of deserialization, they are set to the default values as per data types.

That is all on java serialization and deserialization mechanism. Hope the article was useful.

0
Liked the article ? Spread the word...