Serialization and Deserialization in Java

Table of Contents

Java Serialization and Deserialization enable us to convert objects into a byte stream for storage or transmission, and later reconstruct them into their original form.

In this article, we’ll explore how Serialization and Deserialization work, how to implement them, and highlight best practices, potential pitfalls, and ways to leverage them in real-world applications.

What are Serialization and Deserialization in Java?

  • Serialization: The process of converting an object into a byte stream so that it can be stored in a file, or sent over a network.
  • Deserialization: The reverse process of converting a byte stream back into a Java object.
Workflow of Java Serialization and Deserialization: Converting Objects to Byte Streams and Back for Storage or Transmission

These mechanisms allow Java objects to be stored or transmitted in a format that can later be reconstructed, facilitating persistence when combined with storage solutions.

How do you implement serialization and deserialization in Java?

Serialization and Deserialization are straightforward with Java’s built-in APIs. At the core of this functionality is the Serializable interface, which we’ll explore next.

Serializable Interface in Java

The Serializable interface is a marker interface in Java, meaning it does not contain any methods. Classes must implement Serializable to indicate that their instances can be serialized, While implementing Serializable allows a class to be serialized, all non-transient and non-static fields of the class must also be serializable. If any field references an object that does not implement Serializable, a NotSerializableException will be thrown.

Example:

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    // Constructor, getters, and setters
}

Serializing an Object Using ObjectOutputStream

Serialization is performed using the ObjectOutputStream class. Here’s how to serialize an object:

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializationExample {
    public static void main(String[] args) {
        Person person = new Person("John Doe", 30);

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) 
        {
            oos.writeObject(person);
            System.out.println("Object serialized successfully.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

This code demonstrates how to serialize a Person object by converting it into a byte stream and saving it to a file named person.ser. The ObjectOutputStream is used for the serialization process, and the try-with-resources block ensures the stream is closed automatically.

Deserializing an Object Using ObjectInputStream

Deserialization uses the ObjectInputStream class to read the byte stream and reconstruct the object.

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializationExample {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person person = (Person) ois.readObject();
            System.out.println("Deserialized Object: " + person.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

This code illustrates the deserialization process, where a previously serialized Person object is read from the file person.ser and reconstructed in memory. The ObjectInputStream is used to read the object, and the try-with-resources block ensures the stream is properly closed.

What is a serialVersionUID and why should I use it?

The serialVersionUID is a unique identifier used during the serialization and deserialization of Java objects to verify compatibility between the sender and receiver of a serialized object. If a class is modified (e.g., by adding or removing fields) after serialization, and the serialVersionUID has not been explicitly defined, the JVM generates one based on the class structure. This can lead to a mismatch if the class changes, resulting in a InvalidClassException during deserialization. By explicitly declaring a serialVersionUID, developers can control this versioning process and ensure compatibility.

For example, consider a class without a defined serialVersionUID:

public class Book implements Serializable {

    private String name;
    private String author;
    private double price;
    
    // getters , setters , constructors
}

If we serialize this class and later modify it (e.g., by adding a new field), deserializing the older serialized object throws an exception.

java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = -2892936963561990304, local class serialVersionUID = 7474211501842758058

To avoid this, explicitly define serialVersionUID in the class:

public class Book implements Serializable {
    private static final long serialVersionUID = 1L; 
    private String name;
    private String author;
    private double price;
    // getters , setters , constructors
}

This approach ensures controlled serialization behavior, even if the class evolves.

Note: Keep in mind that when we add or remove a field, the JVM reconstructs the object based on the old version of the serialized class. When we add a new field, it takes its default value, and when we remove a field, the system ignores it even if it exists in the serialized stream.

Deserialization in Java step-by-step

  • Read the Stream

The JVM starts by reading the serialized data stream, which contains metadata about the serialized object. This includes the class name, serialVersionUID, and the sequence of fields and values.

  • Validate the Class

The JVM checks the serialVersionUID in the stream against the serialVersionUID of the current class in memory:

  • If they differ, a java.io.InvalidClassException is thrown, as the class definitions are deemed incompatible.
  • If they match, the JVM proceeds with deserialization.
  • Allocate a New Object

The JVM allocates memory for a new instance of the class but does not call the class’s constructors. This is because constructors might modify fields, and the goal of deserialization is to reconstruct the object state exactly as it was.

  • Read Field Data

The JVM reads the field values from the serialized stream in the order they were written during serialization:

  • Existing Fields: For fields present in both the serialized stream and the current class, we directly assign their values.
  • Added Fields: If we add a field to the current class that doesn’t exist in the serialized stream (e.g., a new field added after serialization), we assign it its default value (e.g., null for objects, 0 for integers, false for booleans).
  • Removed Fields: If we remove a field from the current class but it still exists in the serialized stream (e.g., a field removed after serialization), we ignore its data in the stream.

Transient and Static Fields During Serialization

What is the transient Keyword?

The transient keyword marks a field to be excluded from serialization, meaning its value will not be included in the byte stream during the serialization process.

Why Static and Transient Fields Are Not Serialized?

  • Static fields: Static fields are not serialized because serialization captures the state of an instance, and static fields belong to the class rather than any specific instance.
  • Transient fields: Transient fields, on the other hand, are excluded intentionally, often to skip sensitive or irrelevant data, as marked by the transient keyword.

Example:

import java.io.Serializable;

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient String password;

    // Constructor, getters, and setters
}

Conclusion

Serialization and Deserialization are indispensable tools for Java developers, enabling object persistence and data transfer. By implementing the Serializable interface and understanding key concepts like transient fields and constructors, you can avoid common pitfalls and make your Java applications more robust.

Leave a Reply

Your email address will not be published. Required fields are marked *

Join the Tribe