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.
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.