Java Serialization Overview
Fast recall
Quick revision
Core answer
Serializable objects into a stream that ObjectInputStream can reconstruct, including shared references and cycles. It is a legacy Java-specific mechanism and should not be used for untrusted input or new public data formats.Interview-ready guide
Interview answer
Java serialization is the platform's native mechanism for turning an object graph into a byte stream and reconstructing it later. A class opts in with the marker interface Serializable. ObjectOutputStream.writeObject records class information, field data, and references; ObjectInputStream.readObject rebuilds compatible objects.
The mechanism traverses reachable serializable objects recursively. It assigns handles to objects already written, so two fields that refer to the same object normally refer to the same reconstructed object, and ordinary cycles can be preserved. By default, non-static, non-transient instance fields participate. Static fields belong to the class, and transient fields are not part of the default serialized state.
During ordinary deserialization, constructors and field initializers of serializable classes are not used in the normal way. The no-argument constructor of the first non-serializable superclass is invoked, so it must be accessible to the serializable subclass. Classes can customize state with methods such as writeObject and readObject, but that increases the compatibility and security surface.
For example, an in-memory test can serialize an array containing the same Address object twice. After reading it, the two array positions still refer to one reconstructed instance. That demonstrates graph identity rather than merely copying field values.
Native serialization is Java-specific, tightly coupled to class structure, and dangerous with untrusted input because object construction hooks and reachable classes can execute unexpected behavior. It is reasonable only in controlled legacy or private contexts with strict allow-list filtering and limits. New APIs and persisted contracts normally use an explicit schema or data format whose fields, validation, and evolution rules are visible.
Java Serialization Overview
Direct Answer
Java native serialization converts a graph of Serializable objects into a stream that ObjectInputStream can reconstruct, including shared references and cycles. It is a legacy Java-specific mechanism and should not be used for untrusted input or new public data formats.
Deep Dive & Architecture
The first occurrence of an ordinary object is written with a new handle. Later references can point back to that handle, which preserves aliasing and permits cycles. Independently copying every field would change program meaning: two references to one mutable object would become two separate objects after reading.
Default field discovery is based on the serializable class hierarchy. Static members are class state and do not belong to one serialized instance. Transient members are intentionally omitted. The stream can include class descriptors and version identifiers, but it does not provide a safe, language-neutral schema contract comparable to formats designed for interoperability.
Deserialization is more powerful than parsing plain data. It allocates objects, restores private state, and may invoke class-specific hooks such as readObject, readResolve, validation callbacks, or code in reachable library types. That execution model is the core reason an attacker-controlled stream is unsafe. A successful type cast after readObject happens too late to remove the danger.
If a legacy internal stream must be read, restrict the source, apply ObjectInputFilter allow-lists and graph limits before reading, keep dependencies patched, and authenticate the surrounding data where needed. Filters reduce exposure but do not turn native deserialization into a recommended public input format. Prefer a deliberately parsed representation for new systems.
Code Example
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class SerializationGraph {
static final class Address implements Serializable {
private static final long serialVersionUID = 1L;
final String city;
Address(String city) { this.city = city; }
}
public static void main(String[] args) throws Exception {
Address shared = new Address("Pune");
byte[] bytes;
try (var buffer = new ByteArrayOutputStream();
var output = new ObjectOutputStream(buffer)) {
output.writeObject(new Address[] {shared, shared});
output.flush();
bytes = buffer.toByteArray();
}
try (var input = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
Address[] restored = (Address[]) input.readObject();
if (restored[0] != restored[1]) throw new AssertionError("identity lost");
System.out.println(restored[0].city);
}
}
}
Check your understanding
Answer these without looking back at the article.
- Which fields participate in default serialization?
- How are shared references and cycles represented?
- Why is deserializing untrusted data unsafe even before a cast?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.