Object Serialization and Deserialization
Java offers high-level streams DataInputStream and DataOutputStream to read and write primitive data types. Similarly, Java offers high-level streams ObjectInputStream and ObjectOutputStream, which, when chained to low-level streams such as FileInputStream and FileOutputStream, can be used by programs to read and write objects.
The process of writing an object to a persistent storage area, such as a file is called Object Serialization, and the process of reading a serialized object back into the program is called Deserialization. The goal here is to save the state of an object. Even without serialization, we could use the I/O classes to save the object state and restore it at a later time. However, all programmers would need to do it in their own way, and the process would be prone to errors. Serialization standardizes and automates the process of saving the state of an object, and hence makes it robust.
Serialization is the process of writing the state of an object to a byte stream. This is useful when we want to save the state of our program to a persistent storage area, such as a file. At a later time, we may restore these objects by using the process of deserialization. |
To make the objects of a class serializable, the class must implement the interface Serializable:
class TestSerialClass implements Serializable {
// body of the class
}
The Serializable interface is an empty interface (i.e. no methods are declared inside it) and is used to just tag a class for possible serialization. If a class is Serializable, all of its subclasses are also Serializable. To make our class Serializable in this way is the necessary condition to enable the objects of the class to be serialized. Obviously, we need to do more work to actually serialize the objects.
Writing with ObjectOutputStream
To write an object to a file, we use the ObjectOutputStream to write it to a low-level stream, which in turn will write it to the file. For example, consider the following code fragment:
FileOutputStream out = new FileOutputStream("serial.txt");
ObjectOutputStream os = new ObjectOutputStream(out);
os.writeObject("serialOut");
os.writeObject(new TestSerialClass());
os.writeObject("End of storage!");
os.flush();
Note that ObjectOutputStream must be chained to another stream because it cannot write directly to a device (file or socket). In the preceding example, we chain an ObjectOutputStream to a FileOutputStream, which can write it to a file. Also note that a string and two objects are written to the stream by invoking the writeObject(…) method. If we pass in an object that is not Serializable, the writeObject method will throw a NotSerializableException.
Only the data of the object (along with the class name) is saved, and not the class definition. The static and transient variables are not saved. To be specific, the following are saved in serialization:
- The values of the instance variables of the serialized object.
- The class description of the object, which includes the class name, the serial version unique ID, a set of flags describing the serialization method, and a description of the data fields.
- All the objects that a serialized object refers to through object reference variables. That means those objects must be serializable; otherwise, we will get a compiler error. The objects can be read back in the same order in which they were stored.
Reading with ObjectInputStream
Once we have written objects and primitive data types to a stream, We will likely want to read them again and reconstruct the objects. This is also straightforward. Here is a code fragment that reads in the String and the Date objects that were written to the file named “serial.txt” in the previous example:
FileInputStream in = new FileInputStream("serial.txt");
ObjectInputStream is = new ObjectInputStream(in);
String note = (String)is.readObject();
TestSerialClass serialIn1 = (TestSerialClass)is.readObject();
TestSerialClass serialIn2 = (TestSerialClass)is.readObject();
Just like ObjectOutputStream, ObjectInputStream must be chained to another stream because it cannot read directly from the device (file or socket). In the preceding example, our code chains an ObjectInputStream to a FileInputStream, and uses the ObjectInputStream’s readObject() method to read the two objects and a string that we stored earlier in a file. Note that the objects must be read in the same order in which they were written. Further note that the object returned by the readObject() method is cast to a proper type before assigning it to a declared object reference. Some important points about serialization:
- If a class is serializable, then all the subclasses of this superclass are implicitly serializable even if they don’t explicitly implement the Serializable interface.
- If we want to serialize an array (or some other collection), each of its elements must be Serializable.
- Static variables are not saved as part of serialization. Recall that the purpose of serialization is to save the state of an object, and a static variable belongs to the class and not to an object of the class.
Object Serialization and Deserialization Example
The following program illustrates how to use object serialization and deserialization. It begins by instantiating an object of class Student. This object has three instance variables that are of types int, String, and double. This is the information we want to save and restore.
A FileOutputStream is created that refers to a file named “serial.txt” and an ObjectOutputStream is created for that file stream. The writeObject( ) method of ObjectOutputStream is then used to serialize our object. The object output stream is flushed and closed. A FileInputStream is then created that refers to the file named “serial.txt” and an ObjectInputStream is created for that file stream. The readObject( ) method of ObjectInputStream is then used to deserialize our object. The object input stream is then closed.
Note that Student is defined to implement the Serializable interface. If this is not done, a NotSerializableException is thrown. If we try experimenting with this program by declaring some of the Student instance variables to be transient, that data is then not saved during serialization.
FileTest16.java
// Object serialization and deserialization example
import java.io.*;
class Student implements Serializable {
int roll;
String name;
double grade;
Student(int roll, String name, double grade) {
this.roll = roll;
this.name = name;
this.grade = grade;
}
@Override
public String toString() {
return "Student{" + "roll=" + roll + ", name=" + name + ", grade=" + grade + '}';
}
}
public class FileTest16 {
public static void main(String[] args) {
FileOutputStream fout = null;
ObjectOutputStream oos = null;
FileInputStream fin = null;
ObjectInputStream ois = null;
Student st[] = new Student[3];
Student s;
st[0] = new Student(101, "Radhamadhav Goswami", 8.39);
st[1] = new Student(102, "Ramesh Das", 8.75);
st[2] = new Student(103, "Gopa Ganguly", 9.15);
//object serialization
try {
fout = new FileOutputStream("serial.txt");
oos = new ObjectOutputStream(fout);
oos.writeObject(st[0]);
oos.writeObject(st[1]);
oos.writeObject(st[2]);
System.out.println("Object serialization done..\n");
}
catch(Exception e) {
System.out.println(e);
}
finally {
try {
fout.close();
oos.close();
}
catch(Exception e) {
System.out.println(e);
}
}
//object deserialization
try {
fin = new FileInputStream("serial.txt");
ois = new ObjectInputStream(fin);
System.out.println("Object deserialization started..");
for(int i=0; i<st.length; i++) {
s = (Student)ois.readObject();
System.out.println(s);
}
}
catch(Exception e) {
System.out.println(e);
}
finally {
try {
fin.close();
ois.close();
}
catch(Exception e) {
System.out.println(e);
}
}
}
}
The output is shown here:
C:\file>java FileTest16
Object serialization done..
Object deserialization started..
Student{roll=101, name=Radhamadhav Goswami, grade=8.39}
Student{roll=102, name=Ramesh Das, grade=8.75}
Student{roll=103, name=Gopa Ganguly, grade=9.15}
Summary
The three most important takeaways from the Java I/O tutorial are as follows:
- The low-level FileInputStream/FileOutputStream classes and the high-level DataInputStream/DataOutputStream classes are used to read and write data in binary format.
- The low-level FileReader/FileWriter classes and the high-level BufferedReader/BufferedWriter classes are used to read and write data in text format.
- ObjectInputStream and ObjectOutputStream can be used to read and write objects by chaining these high-level streams to low-level streams such as FileInputStream and FileOutputStream.