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 –
- 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.
- If you want to make a deep copy of an object, you can serialize and then deserialize that object.
- If you want to send an object over the network, you can just serialize the object and send it.
- 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.
- 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. - 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 –
Vehicle
class is non-serializable.Car
is serializable.- 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.