Serialization and Deserialization in Java through concrete examples12 min read
In Java, the lifetime of all objects is limited at runtime, which means when you close a program and reopen it again, every object is reset to the initial state. Information and data which were previously created will also be lost. Sometimes, it’s crucial for us to save the data from objects which we created to some permanent storage for later retrieval.
Java has a mechanism for converting the state of an object into a stream of bytes, the object then can be saved in some permanent storage such as a file or later reconstruction, and this process is called serialization. Deserialization is the reverse process when serialized bytes are converted back to actual Java objects.
We not only can save the serialized object in a file, but the serialization and deserialization process can also be made on networks, however. And because streams of bytes are platform-independent, thus an object serialized on one platform can also be deserialized on another platform, this also means a serialized object in a program can be reconstituted in another program.
Table of Contents
Making a class serializable
To make the serialization process happen, the first thing we need to start with is to make this class serializable. This can only be done by implementing the Serializable
interface:
import java.io.Serializable;
class MyClass implements Serializable {
// some code...
}
You will feel a slight surprise if you look at the Java docs and see that this interface has no method at all. In Java, any interface with no method is called marker interface, even though we don’t have to override anything when implementing these interfaces, but it acts as a beacon to tell the compiler that class implementing it has some special behaviors.
Serializing an object
We now readily serialize an object, in order to do that, I assume you already have fundamental knowledge about byte stream and I/O in Java. We need to use the void writeObject(Object object)
method of the ObjectOutputStream
class. The writeObject()
method accepts an object and writes the current state of this object to the stream. Typically, the serialization for an object can be constructed as follow:
import java.io.*;
class MyClass implements Serializable {
public static void serialize(Object obj, String fileName) throws IOException {
FileOutputStream fos = new FileOutputStream(fileName);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
}
}
Let’s step-by-step break down what happens here:
- First, we create a method named
serialize()
which takes 2 parameters, the object we want to write its state and the file we want to write it to. - In the method signature, we must throw
IOException
when there are some unpredictable errors that can happen during the serialization process. - We create a new
FileOutputStream
objectfos
, which will create an output stream to write to a file which we pass in its constructor. - Next, we use a handy class
BufferedOutputStream
to speed up the writing process. - The
ObjectOutStream
then performs real serialization, we create a newObjectOutStream
objectoos
and passbos
to its constructor which signifies we will write objects to a specifiedOutputStream
namedbos
. - Then we invoke the
writeObject()
to write the Objectobj
to the stream. - Finally, we need to close the stream to avoid resource leaks.
You also can use the try-with-resources syntax, it will automatically close your stream when you’re done.
Deserializing an object
We have learned how to serialize an object and save it to a file, now let’s reconstitute the object previously serialized through the deserialization process:
import java.io.*;
class MyClass implements Serializable {
public static Object deserialize(String fileName) throws IOException, ClassNotFoundException {
Object obj;
FileInputStream fis = new FileInputStream(fileName);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream ois = new ObjectInputStream(bis);
obj = ois.readObject();
ois.close();
return obj;
}
}
As you can see, the code fragment from the deserialization process looks pretty similar to the serialization process. However, here we want to read data from a stream hence we have to create instances that extend from InputStream
abstract class, i.e FileInputStream
, BufferedInputStream
, ObjectInputStream
directly or indirectly extend InputStream
class.
The ObjectInputStream
is salient for us to deserialize an object. Calling the readObject()
method from an ObjectInputStream
instance will give you an object in the input stream. After assigning the result of reading the object state to a variable obj
, we close the stream to avoid resource leak and return this obj
object.
Serialization and Deserialization Example
So far, notably, the ObjectOutputStream
and ObjectInputStream
are high-level stream class containing methods for us to serialize and deserialize an object. Let’s make a concrete example to demonstrate these 2 processes:
import java.io.Serializable;
class Laptop implements Serializable {
private String laptopName;
private int laptopId;
private int numOfCPUs;
private float screenSize;
// getters and setters
@Override
public String toString() {
return "Laptop{" +
"laptopName='" + laptopName + '\'' +
", laptopId=" + laptopId +
", numOfCPUs=" + numOfCPUs +
", screenSize=" + screenSize +
'}';
}
}
In this example above, we create a class named Laptop
and it has a number of different fields. Then, we create a method initLaptops()
inside this class to give us a list of Laptop objects which we will use for the serializing and deserializing process:
public static List<Laptop> initLaptops() {
List<Laptop> laptops = new ArrayList<>();
Laptop laptop1 = new Laptop();
laptop1.setLaptopName("ASUS");
laptop1.setLaptopId(123);
laptop1.setNumOfCPUs(2);
laptop1.setScreenSize(13.3F);
laptops.add(laptop1);
Laptop laptop2 = new Laptop();
laptop2.setLaptopName("Macbook");
laptop2.setLaptopId(456);
laptop2.setNumOfCPUs(4);
laptop2.setScreenSize(15F);
laptops.add(laptop2);
return laptops;
}
Now, let’s make the serialization and deserialization processes happened! Inside the main()
method we will call the serialize()
and deserialize()
methods sequentially which we defined in the MyClass
class:
public static void main(String[] args) {
String fileName = "laptops.txt";
try {
MyClass.serialize(initLaptops(), fileName);
List<Laptop> laptops = (List<Laptop>) MyClass.deserialize(fileName);
for(Laptop laptop: laptops) {
System.out.println(laptop.toString());
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
Let’s carefully examine what happens here:
- First, we create a file name that will store the serialized object.
- Within the try-catch block, we serialize the list of laptops and store its state to the file as we pass in on the second argument. After crossing this line and if there is no exception occurred, the serialized will been saved to the file
laptops.txt
. - Now we are eligible for deserializing the object. Notice that the type of the object when serializing and deserializing should be identical, hence the safe-casting is required.
- Finally, we print out the deserialized object which we save in the variable
laptops
.
This is what on the console if we run this code:
Laptop{laptopName='ASUS', laptopId=123, numOfCPUs=2, screenSize=13.3}
Laptop{laptopName='Macbook', laptopId=456, numOfCPUs=4, screenSize=15.0}
If you are curious, you can open the serialized file laptops.txt
and perhaps you will get something like this:
��srjava.util.ArrayListx����a�IsizexpwsrLaptopA]a֗�IlaptopIdI numOfCPUsF
screenSizeL
laptopNametLjava/lang/String;xp�AptMacbooksq~px
Serialization and IS-A Relationship
The IS-A relationship in Java is concerned with inheritance or generalization. If a parent class implements Serializable
, then all of its subclasses will also be serializable as well. For example, we have a superclass called Beverage
and a subclass Juice
:
class Beverage implements Serializable {
// implementation goes here...
}
class Juice extends Beverage {
// some sloppy code...
}
So in this case, even the Juice
class doesn’t implement the Serializable
interface, but it inherits this property from its parent. Consequently, the Juice
class also can be serialized.
Serialization and HAS-A Relationship
If a class has a reference in another class, all references must implement Serializable
, otherwise the NotSerializableException
will arise a runtime. For example, from my previous Laptop
class, now I want to add one additional field to it, this field will give me some information about the RAM of this laptop:
import java.io.Serializable;
class Laptop implements Serializable {
private String laptopName;
private int laptopId;
private int numOfCPUs;
private float screenSize;
private RAM ramInfo; // newly added field
// code surpassed for brevity
}
Here is our RAM class:
class RAM {
private String ramType;
private byte ramSize;
private String ramStatus;
private int ramSpeed;
// getters and setters
}
If I forget implement the Serializable
interface in the RAM
class, then the serializing process in the Laptop
class cannot happen and an exception will occur at run time:
public static List<Laptop> initLaptops() {
List<Laptop> laptops = new ArrayList<>();
Laptop laptop1 = new Laptop();
// code surpassed
RAM ram1 = new RAM();
ram1.setRamType("DDR3");
ram1.setRamSize("8GB");
ram1.setRamSpeed("1867 MHz");
ram1.setRamStatus("OK");
laptop1.setRamInfo(ram1);
laptops.add(laptop1);
// laptop2 is truncated for brevity
return laptop;
}
public static void main(String[] args) {
String fileName = "laptops.txt" ;
try {
MyClass.serialize(initLaptops(), fileName); // the NotSerializableException will occur here because RAM class doesn't implement Serializable
// code surpassed
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
To tackle this problem, we simply implement the Serializable
interface on the RAM
class.
Making fields not serializable
Sometimes, we want some fields to be serialized, but some others aren’t. In this case, we can make the use of the transient
keyword to prevent some particular fields to be serialized. Let’s use the Laptop
class example again to substantiate this process:
class Laptop implements Serializable {
private String laptopName;
private int laptopId;
private int numOfCPUs;
private transient float screenSize; // add transient keyword to prevent it from serializing.
// code surpassed
}
The code stays intact as in the Serialization and Deserialization Example section, except I added the transient
keyword to the screenSize
field to preclude it from serializing. Hence when deserializing back, here is what we got:
Laptop{laptopName='ASUS', laptopId=123, numOfCPUs=2, screenSize=0.0}
Laptop{laptopName='Macbook', laptopId=456, numOfCPUs=4, screenSize=0.0}
Both screenSize
fields of these 2 objects haven’t serialized at all, the value of 0.0 displayed here because when we call toString()
method on each laptop, the variable with the type of float at the class scope will have the default value of 0.0.
SerialVersionUID
For now, we have learned how to serialize an object into bytes and save it to a file, and get it back through the deserialization process. However, one question might arise, how can you ensure the class remains unchanged through the serialization and deserialization process? What would happen if I serialize an object on one day, later on, I modify some fields of this class and then deserializing to get the object back? Let’s examine an example below:
import java.io.Serializable;
class Book implements Serializable {
private int id;
private String authorName;
private String title;
}
I then serialize data on this class to the file name book.txt
. A day after, a person modify your code as follow:
import java.io.Serializable;
class Book implements Serializable {
private String authorName;
private String title;
private int numOfPages;
}
He removed the id
field and add a new filed numOfPages
, and then he tried to restore the previously saved object. The code didn’t raise any error at all and gave him a book object, however, the serialized object had a different format compared with the current Book
class.
To ensure the consistency during the serialization and deserialization process, SerialVersionUID
comes into play. But what is that? Is it a method, a class, or something? No, it’s simply just a field act like a version control which is used to verify that the sender (the person who serializes the object) and the receiver (the person who deserializes the object) of a serialized object are compatible and have loaded the classes for that object.
A serialized class can declare its own serialVersionUID
by explicitly declaring a field named serialVersionUID
with static
, final
and type long
like this:
static final long serialVersionUID = 1L;
It is recommended to use the most strict access modifier private
when possible, this will make sure this serialVersionUID
only affects the declaring class.
If the serialVersionUID
of the sender and receiver doesn’t identical, then the InvalidClassException
will occur. Let’s unravel it with the Book
class:
import java.io.Serializable;
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String authorName;
private String title;
public Book(int id, String authorName, String title) {
this.id = id;
this.authorName = authorName;
this.title = title;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", authorName='" + authorName + '\'' +
", title='" + title + '\'' +
'}';
}
// getters and setters
}
And SerializationUtils
class for serialization and deserialization process:
import java.io.*;
public class SerializationUtils implements Serializable {
public static void serialize(Object obj, String fileName) throws IOException {
FileOutputStream fos = new FileOutputStream(fileName);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
}
public static Object deserialize(String fileName) throws IOException, ClassNotFoundException {
Object obj;
FileInputStream fis = new FileInputStream(fileName);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream ois = new ObjectInputStream(bis);
obj = ois.readObject();
ois.close();
return obj;
}
public static void main(String[] args) {
Book book = new Book(123, "Harper Lee", "To Kill A Mockingbird");
// serialization process
try {
serialize(book, "book.txt");
} catch (IOException e) {
e.printStackTrace();
}
// deserialization process
try {
Book deserializedBook = (Book) deserialize("book.txt");
System.out.println(deserializedBook.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Once we run this code, we procure the object saved in the book.txt
file and read it back from the deserialization process:
Book{id=123, authorName='Harper Lee', title='To Kill A Mockingbird'}
Now let’s see what happens if we change the serialVersionUID
of the Book
class to a new one:
private static final long serialVersionUID = 69L;
If we rerun the deserialization process again, the InvalidClassException
will be thrown:
java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 69
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430)
at SerializationUtils.deserialize(SerializationUtils.java:17)
at SerializationUtils.main(SerializationUtils.java:27)
The serialVersionUID
is not required, however, it is strongly recommended for a serializable class to explicitly declare its own serialVersionUID
. By doing so, this number warrants a consistent serialVersionUID
value across different Java compiler implementation. At the same time, there is no need for two different classes to have unique serialVersionUID
number, they can be the same.
Summing Up
It’s such a long article, I’m glad that you read till this point. Let’s summarize what we have obtained:
- Serialization is the process of converting an object into bytes and then save it somewhere such as a file.
- Deserialization is the reverse process where we reconstitute an object back from a bytes file.
- To make a class serializable, this class must implement the Serializable interface.
ObjectOutputStream
andObjectInputStream
accordingly provide thewriteObject()
andreadObject()
which are used for serialization and deserialization processes, respectively.- A subclass is eligible for serializing without explicitly implementing the
Serializable
interface if its parent already did it. - We can prevent a field from serializing by using the
transient
keyword. - The
serialVersionUID
number is a kind of version control that keeps the consistency between the sender and receiver class during the serialization and deserialization process.
.