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.

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 object fos, 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 new ObjectOutStream object oos and pass bos to its constructor which signifies we will write objects to a specified OutputStream named bos.
  • Then we invoke the writeObject() to write the Object obj 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 and ObjectInputStream accordingly provide the writeObject() and readObject() 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.

.

Previous Article
Next Article
Every support is much appreciated ❤️

Buy Me a Coffee