Serialization

Introduction

Serialization is a mechanism to convert the state of an object into a byte stream. This byte stream can be saved to a database or a file or we can transfer over a network.

Deserialization is the exact opposite process of serialization where the byte stream is converted back to an object.

This serialization and deserialization process is platform independent, meaning an object can be serialized on one platform and deserialized on an entirely different platform.

Why do we need serialization?

There could be multiple reasons. For example –

  1. Let’s assume you have created one war game and you want to add a feature to save and resume the game so that users don’t have to start from the beginning every time. To achieve that, you can serialize the objects of the game to a file during save and deserialize them during resume.
  2. If you want to make a deep copy of an object, you can serialize and then deserialize that object.
  3. If you want to send an object over the network, you can just serialize the object and send it.
  4. If you want to send an object to another computer or JVM, you can just serialize the object and send it.
Can we serialize every object?

Before answering this, let’s try to Serialize a Car instance. 

We will make the field public and assign a value for simplicity. But in actual application, never do that. Always make the instance variable private and provide a public getter and setter method.

public class Car {
    public int wheelCount = 4;
    public String toString() {
        return "Car wheelCount = " + wheelCount;
    }
}

We can use the ObjectOutputStream.writeObject() method to write an object to the output stream.

Car obj = new Car();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(obj);
out.close();
System.out.println(Arrays.toString(baos.toByteArray()));

Output:

Exception in thread "main" java.io.NotSerializableException: Car

As you can see, we cannot serialize every object. If a class implements java.io.Serializable interface, then only that class is serializable. If any superclass of a class is serializable, then there is no need to implement the Serializable interface as the subclass is automatically serializable. This Serializable interface is a marker interface meaning this interface doesn’t contain any method. The sole purpose of this interface is to mark a class as serializable.

public interface Serializable { }

So, let’s mark the Car class as Serializable.

public class Car implements Serializable {
    public int wheelCount = 4;
    public String toString() {
        return "Car wheelCount = " + wheelCount;
    }
}

Now, if you execute the same code, it will work fine and will print the output something like –

[-84, -19, 0, 5, 115, 114, 0, 3, 67, 97, 114, 89, 104, -9, 13, -6, 2, 26, -106, 2, 0, 1, 73, 0, 10, 119, 104, 101, 101, 108, 67, 111, 117, 110, 116, 120, 112, 0, 0, 0, 4]

To deserialize the serialized object, we can use ObjectInputStream.readObject().

Car obj = new Car();

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(obj);
out.close();
byte[] byteArray = baos.toByteArray();

ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
ObjectInputStream in = new ObjectInputStream(bais);
Car car = (Car) in.readObject();
in.close();
System.out.println(car);

Output:

Car wheelCount = 4

This entire serialization and deserialization happened in memory. It is also allowed to write the byte stream in a file.

How to save serialized data in a file and read from that file?

Let’s use the same Car class. This is quite similar to the earlier example. We will write the byte stream into a file called test.ser file. You can use any extension of your choice (like .txt or .abc etc.) but .ser extension is preferred as by looking at the file extension you can identify this file is containing a serialized object.

Car obj = new Car();

FileOutputStream fos = new FileOutputStream("test.ser");  
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(obj);
out.close();

ObjectInputStream fis = new ObjectInputStream(new FileInputStream("test.ser"));  
Car car = (Car) fis.readObject();  
fis.close();
System.out.println(car);

Output:

Car wheelCount = 4
When we serialize an object, do all fields get serialized?

Almost all, but not all.

  1. The static fields are not serialized. Because static variables belong to class, not to objects. So, when we deserialize from the serialized state, static variables will be assigned with value whatever the current class has.
  2. Fields marked as transient are not serialized. When we deserialize from the serialized state, transient variables are initialized with default values. For object reference it would be null, for boolean it would be false etc.
If an object contains a reference variable, does that get serialized too?

Yes. serialization saves the entire object graph. So all the objects referred by instance variables are also serialized. Also either all the objects get serialized or the serialization fails.

To test this, add a reference variable to the Car class.

public class Car implements Serializable {
    public int wheelCount = 4;
    public Tyre type = new Tyre();

    public String toString() {
        return "Car wheelCount = " + wheelCount + ", tyre manufacturer = " + type.manufacturer;
    }
}
public class Tyre {
    public String manufacturer = "MRF";
}

If you try to serialize an Car object, you will get an exception –

Exception in thread "main" java.io.NotSerializableException: Tyre

The reason is pretty simple. As part of the serialization process, along with the object, all the objects referred by instance variables are also serialized. So they also must be serializable. So, to make our code work, the Tyre class must implement the Serializable interface.

public class Tyre implements Serializable {
    public String manufacturer = "MRF";
}

Now, if you try to serialize or deserialize a Car object, that will work just fine without any error.

Please note, during serialization, if multiple reference variables refer to the same object in heap, only one copy is saved and during serialization and during deserialization, that single copy is restored and that object is assigned to all those reference variables.

Does the deserialization process call constructors?

When we deserialize an object, a new object is created without calling the constructor. If the constructor is called, that will reset the object’s state to its new state. But as we want to preserve the saved state, constructor is not called.

But if there are non-serializable super classes in the inheritance tree, constructor will be called for those classes and their state will be reinitialized to its new state. To understand this, let’s create 3 classes.

public class Vehicle {
    private String description;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}
public class Car extends Vehicle implements Serializable {
    private int wheelCount;

    public void setWheelCount(int wheelCount) {
        this.wheelCount = wheelCount;
    }
    
    public int getWheelCount() {
        return wheelCount;
    }
}
public class BMW extends Car {
    private int seatCount;
    
    public BMW(int seatCount) {
        this.seatCount = seatCount;
    }
}

As you can see –

  1. Vehicle class is non-serializable.
  2. Car is serializable.
  3. As BMW extends Car, BMW is also serializable.

If you serialize a BMW object and deserialize it, the new BMW object will retain instance variable values of BMW class and Car class as both are serializable. But as Vehicle is non-serializable, the constructor will be called and the description instance variable will be initialized with null.

serialVersionUID

Consider serialVersionUID as a version number of a serializable class. We normally define it as a static final long field. We mark it as final because there is no need to change its value and static because it is applicable to class, not to any particular instance of that class.

You may ask, wait.. staic fields are not supposed to be serialized. You are right. But this is one exception.

Note that the serialVersionUID is optional. That means, if you do not explicitly declare it, the Java compiler will generate one for you. The value of this auto-generated serialVersionUID is calculated based on elements of the class like instance variables, methods, constructors, etc. Now the question is, if it is taken care of by the Compiler, why should we bother to declare a serialVersionUID explicitly?

To understand this, let’s create a Car class without declaring serialVersionUID.

public class Car implements Serializable {
    public int wheelCount = 4;
}

Serialize an object of this class.

Car obj = new Car();
FileOutputStream fos = new FileOutputStream("test.ser");  
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(obj);
out.close();

Before deserialization, add a new field to the Car class.

public class Car implements Serializable {
    public int wheelCount = 4;
    public int topSpeed = 150;
}

Now try to deserialize.

ObjectInputStream fis = new ObjectInputStream(new FileInputStream("test.ser"));  
Car car = (Car) fis.readObject();  
fis.close();

You will get an exception similar to following –

Exception in thread "main" java.io.InvalidClassException: Car; local class incompatible: stream classdesc serialVersionUID = 6442670906354571926, local class serialVersionUID = -2860941729156133909

Let’s understand why this InvalidClassException occurred. Value of serialVersionUID is used to determine if a class has been changed since the object was serialized. If the value is same for the serialized object and the class, it is assumed that either the class is not modified or the modification will not affect the deserialization process. So, it is safe to deserialize the object to the current class structure.

As we are relying on auto generated serialVersionUID and added one new field, the serialVersionUID will be changed for the Car class. Now if you try to deserialize, serialVersionUID will not be matched and JVM will throw InvalidClassException. That’s why it’s recommended to add a serialVersionUID explicitly for a serializable class.

If you are using an IDE like Eclipse, it’s easy. If any serializable class doesn’t add a serialVersionUID explicitly, Eclipse will show a warning. Just click on that warning icon and generate the serialVersionUID.

After adding this field, if you try similar steps (serialize ⇒ add fields ⇒ deserialize), it will work. Because the serialVersionUID will be the same for the serialized object and the Car class. So, the deserialization operation will be assumed safe.

If we explicitly define serialVersionUID, is any kind of change allowed in the class after serialization?

No. Some changes are not allowed. Below table summarizes a few common changes.

Change Is Allowed Note
Add new field Yes New field will be initialized with a default value.
Change access modifier of a field (e.g. public to private) Yes The access modifier has no effect on serialization or deserialization process.
Change a field from static to non static Yes As the static field doesn’t take part in the serialization process, this is equivalent to adding a new field.
Change a field from transient to non transient Yes As the transient field doesn’t take part in the serialization process, this is equivalent to adding a new field.
Change the type of a field No Type of data will be incompatible.
Delete a field Yes This field value will be ignored during deserialization.
Change a field from non static to static Yes This field value will be ignored during deserialization.
Change a field from non transient to transient Yes This field value will be ignored during deserialization.
Externalizable

If we want to customize the serialization or deserialization process, we can use the Externalizable interface. This interface consist of two methods – 

  • writeExternal(): Write object into stream.
  • readExternal(): Read object from stream.

Example

public class Car implements Externalizable {
    private int wheelCount;
    private String color;
    
    public Car() {}
    
    public void setWheelCount(int wheelCount) {
        this.wheelCount = wheelCount;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return "Car [wheelCount=" + wheelCount + ", color=" + color + "]";
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(color);
        out.writeInt(wheelCount);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        color = (String)in.readObject();
        wheelCount = in.readInt();
    }
}
Car obj = new Car();
obj.setWheelCount(4);
obj.setColor("Red");
System.out.println("Before : " + obj);

FileOutputStream fos = new FileOutputStream("test.ser");  
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(obj);
out.close();

ObjectInputStream fis = new ObjectInputStream(new FileInputStream("test.ser"));  
Car car = (Car) fis.readObject();  
fis.close();
System.out.println("After : " + car);

Output:

Before : Car [wheelCount=4, color=Red]
After : Car [wheelCount=4, color=Red]

That’s it for now. Hope you have enjoyed this tutorial. If you have any doubt, please ask in the comment section. I will try to answer that as soon as possible. Till then, bye bye.