By WebNest Studio Engineering Team
Java - Core Tutorial
Serialization
Serialization is the process of converting an object's state into a byte stream so it can be saved to a file, sent over a network, or stored elsewhere, and later reconstructed through deserialization. Java supports this natively through the Serializable marker interface and the ObjectOutputStream/ObjectInputStream classes.
Why it matters
Serialization is how an object leaves the JVM: to a file, a cache such as Redis, an HTTP session store or another service. Java's built-in mechanism is in a great deal of existing code, and the ideas behind it, such as which fields are saved, what happens when the class changes, and why untrusted input is dangerous, apply equally to JSON and every other format.
The Serializable Marker Interface
java.io.Serializable declares no methods at all — it is a "marker interface" that simply tells the JVM this class is allowed to be serialized. If an object's class (or any superclass, for non-static fields) does not implement Serializable, attempting to serialize it throws a NotSerializableException at runtime. Every field referenced by the object must itself be serializable (or null), including nested objects.
Excluding Fields with transient
The transient keyword marks a field that should be skipped during serialization — useful for sensitive data (like passwords), derived/cacheable values that can be recomputed, or fields holding non-serializable resources (like open sockets or threads). Transient fields are written out as their type's default value (0, false, or null) when the object is deserialized.
serialVersionUID and Version Control
serialVersionUID is a static final long field used to verify that a serialized object's class version matches the version used to deserialize it. If you don't declare one explicitly, the JVM generates one automatically based on the class's structure — but that generated value changes if the class is modified later, which can break deserialization of previously saved data. Declaring it explicitly (private static final long serialVersionUID = 1L;) gives you control over compatibility across class changes.
ObjectOutputStream and ObjectInputStream
ObjectOutputStream.writeObject(obj) serializes an object to an underlying stream (typically a FileOutputStream), and ObjectInputStream.readObject() reads it back, returning an Object that must be cast to the original type. Both should be used with try-with-resources so the underlying stream is always closed properly.
Example
Serializing and deserializing an object with a transient field
import java.io.*;
class Employee implements Serializable {
private static final long serialVersionUID = 1L;
String name;
transient String password; // excluded from serialization
Employee(String name, String password) {
this.name = name;
this.password = password;
}
}
public class SerializationDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Employee emp = new Employee("Asha", "secret123");
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("emp.ser"))) {
out.writeObject(emp);
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("emp.ser"))) {
Employee restored = (Employee) in.readObject();
System.out.println("Name: " + restored.name);
System.out.println("Password: " + restored.password);
}
}
}
Name: Asha
Password: null
Common Mistakes
- Forgetting to implement Serializable on a class (or a referenced field's class) before serializing it, causing a NotSerializableException at runtime.
- Expecting a transient field to retain its original value after deserialization — it is always reset to its type's default value (null, 0, or false).
- Not declaring an explicit serialVersionUID, then later modifying the class and finding old serialized files can no longer be deserialized due to a UID mismatch.
- Forgetting to close ObjectOutputStream/ObjectInputStream (or not using try-with-resources), which can leave data unflushed or file handles open.
Exercise
The program fails with NotSerializableException because a Customer holds an Address that cannot be serialized. Change the classes so the whole object can be saved and restored, and give Customer an explicit serialVersionUID.
Expected output: Asha, Pune
import java.io.*;
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Customer implements Serializable {
String name;
Address address;
Customer(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class SaveCustomer {
public static void main(String[] args) throws Exception {
Customer original = new Customer("Asha", new Address("Pune"));
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(original);
}
Customer restored;
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
restored = (Customer) in.readObject();
}
System.out.println(restored.name + ", " + restored.address.city);
}
}
Show hints
- Every object reachable from the one being written must itself be serializable.
- The version field is declared as
private static final long serialVersionUID = 1L;
Show solution
import java.io.*;
class Address implements Serializable {
private static final long serialVersionUID = 1L;
String city;
Address(String city) {
this.city = city;
}
}
class Customer implements Serializable {
private static final long serialVersionUID = 1L;
String name;
Address address;
Customer(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class SaveCustomer {
public static void main(String[] args) throws Exception {
Customer original = new Customer("Asha", new Address("Pune"));
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(original);
}
Customer restored;
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
restored = (Customer) in.readObject();
}
System.out.println(restored.name + ", " + restored.address.city); // Asha, Pune
}
}
Quick quiz
1. How many methods does the Serializable interface declare?
- A. None; it is a marker interface
- B. One: serialize()
- C. Two: writeObject() and readObject()
- D. Three
Show answer
A. Implementing it only marks the class as allowed to be serialized.
2. An object is saved, the class is then changed and its serialVersionUID is different. What happens when the old data is read?
- A. The new fields are null
- B. The old class is loaded
- C. InvalidClassException is thrown
- D. ClassCastException is thrown
Show answer
C. The UID in the stream must match the UID of the class that is loaded.
3. Is the constructor of a Serializable class called when an object of it is deserialized?
- A. Yes, always
- B. Only if it is public
- C. Only the no-argument constructor
- D. No; the object is rebuilt from the stream without running its constructor
Show answer
D. Only the no-argument constructor of the nearest non-serializable superclass runs.
Interview questions
What is serialVersionUID and why declare it explicitly?
Show answer
It is a version number for a serializable class, written into the stream with each object and checked when the object is read. If it is not declared, the JVM computes one from the structure of the class, and almost any change to the class, such as adding a method, changes it, making previously saved data unreadable. Declaring it yourself lets you make compatible changes, such as adding a field, without breaking old data.
What is the difference between Serializable and Externalizable?
Show answer
With Serializable the JVM saves and restores all non-transient fields automatically. Externalizable extends it and requires the class to implement writeExternal and readExternal, giving full control over what is written. An Externalizable class must have a public no-argument constructor, which is called during deserialization.
Why is Java serialization avoided for data from untrusted sources?
Show answer
Deserialization creates objects of whatever classes the stream names and runs code in them as it does so. A crafted stream can combine classes already on the classpath to execute commands, and this has been the cause of many serious vulnerabilities. For data exchanged with other systems, formats such as JSON are used instead, and where Java serialization remains, a deserialization filter restricts the classes that may be read.
Key Points to Remember
- Serializable is a marker interface with no methods; it simply permits a class to be serialized.
- transient fields are skipped during serialization and reset to default values on deserialization.
- serialVersionUID should be declared explicitly to control compatibility across class versions.
- ObjectOutputStream.writeObject() and ObjectInputStream.readObject() perform the actual serialization and deserialization.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering