Serialization
Java serialization is the mechanism for converting an object graph into a sequence of bytes that can be stored to a file, transmitted over a network, or persisted in a database. An object is serializable if its class implements the java.io.Serializable marker interface, which has no methods — it serves only as a type token that grants the JVM permission to serialize instances of the class. The serialization process is performed by ObjectOutputStream.writeObject(), which traverses the object graph recursively, encoding each object's class descriptor and field values into a binary stream in a platform-independent format. The format captures the full object graph — if two references point to the same object, it is written once and both references are restored correctly on deserialization. This entry covers the Serializable marker interface and what it commits to, the binary format structure and version negotiation via serialVersionUID, how the serialization engine traverses object graphs and handles cycles and shared references, the customization hooks writeObject and readObject, the serialization proxy pattern for robust versioning, security implications of deserialization, and when to use Java serialization versus alternatives.
The Serializable Contract and serialVersionUID
// ── Basic Serializable class ──────────────────────────────────────────
import java.io.*;
public class Person implements Serializable {
// ALWAYS declare serialVersionUID explicitly:
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email;
// All non-static, non-transient fields are serialized by default
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
@Override public String toString() {
return "Person{name=" + name + ", age=" + age + ", email=" + email + "}";
}
}
// ── Serializing to a file ─────────────────────────────────────────────
Person alice = new Person("Alice", 30, "alice@example.com");
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("person.ser")))) {
oos.writeObject(alice); // writes class descriptor + field values
System.out.println("Serialized: " + alice);
}
// ── Serializing multiple objects to the same stream ───────────────────
List<Person> people = List.of(
new Person("Alice", 30, "alice@example.com"),
new Person("Bob", 25, "bob@example.com"),
new Person("Carol", 35, "carol@example.com")
);
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("people.ser")))) {
for (Person p : people) {
oos.writeObject(p); // each object written sequentially
}
}
// ── serialVersionUID: explicit control of version compatibility ────────
public class BankAccount implements Serializable {
private static final long serialVersionUID = 1L; // version 1
private String accountNumber;
private double balance;
// Adding fields in version 2 with same serialVersionUID = 1L:
// private String currency = "USD"; // OK: new field gets default on deserialization
// Changing field type from double to BigDecimal: INCOMPATIBLE — increment to 2L
}
// ── Automatic serialVersionUID computation (risky): ───────────────────
public class NoVersionUID implements Serializable {
// No explicit serialVersionUID — JVM computes from:
// class name, interface names, field names+types, method signatures
private String data; // Adding any method changes the computed UID — breaks deserialization
}
// ── IDE warning: -Xlint:serial catches missing declarations ───────────
// javac -Xlint:serial MyClass.java
// warning: [serial] serializable class Person has no definition of serialVersionUIDObject Graph Traversal, Cycles, and writeObject/readObject
// ── Object graph with shared references ──────────────────────────────
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private List<Person> members = new ArrayList<>();
public void addMember(Person p) { members.add(p); }
}
Person alice = new Person("Alice", 30, "alice@example.com");
Department dept = new Department("Engineering");
dept.addMember(alice);
dept.addMember(alice); // same object referenced twice
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("dept.ser")))) {
oos.writeObject(dept); // alice serialized ONCE — second reference is a handle
}
// After deserialization: both list entries reference the same Person object ✓
// ── Circular references are handled correctly ─────────────────────────
public class Node implements Serializable {
private static final long serialVersionUID = 1L;
String value;
Node next;
Node prev; // bidirectional list — creates cycles
}
Node n1 = new Node(); n1.value = "A";
Node n2 = new Node(); n2.value = "B";
n1.next = n2; n2.prev = n1; // cycle: n1 → n2 → n1
try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
oos.writeObject(n1); // no StackOverflowError — cycles handled via handle table
System.out.println("Circular graph serialized successfully");
}
// ── writeObject / readObject customization ────────────────────────────
public class SecureRecord implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient: excluded from default serialization
private transient String derivedKey; // computed field — not serialized
public SecureRecord(String username, String password) {
this.username = username;
this.password = password;
this.derivedKey = deriveKey(password);
}
// Custom serialization: encrypt password before writing
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // writes 'username' (non-transient fields)
String encrypted = encrypt(password);
oos.writeObject(encrypted); // write encrypted password as extra data
}
// Custom deserialization: decrypt password after reading
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // reads 'username'
String encrypted = (String) ois.readObject();
this.password = decrypt(encrypted);
this.derivedKey = deriveKey(password); // re-derive computed field
}
private String encrypt(String s) { return "ENC:" + s; } // placeholder
private String decrypt(String s) { return s.substring(4); }
private String deriveKey(String pw) { return "KEY:" + pw.hashCode(); }
}
// ── serialPersistentFields: explicit field list ────────────────────────
public class LegacyClass implements Serializable {
private static final long serialVersionUID = 1L;
// Explicit serialized field list — only 'id' and 'name' are serialized:
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("id", Integer.TYPE),
new ObjectStreamField("name", String.class)
};
private int id;
private String name;
private int internalCache; // excluded — acts like transient
}Security, Serialization Proxy, and Alternatives
// ── Security: ObjectInputFilter to whitelist classes ──────────────────
import java.io.ObjectInputFilter;
// System-wide filter (JVM startup property):
// -Djdk.serialFilter=com.example.**;java.util.*;!*
// Accepts com.example and java.util classes; rejects everything else
// Programmatic filter on a specific stream:
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("data.ser")))) {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.Person;com.example.Department;" + // whitelist
"java.util.ArrayList;java.lang.String;" +
"!*" // reject everything not explicitly whitelisted
);
ois.setObjectInputFilter(filter);
Object obj = ois.readObject(); // filter checked for every class in the graph
}
// ── Serialization proxy pattern ────────────────────────────────────────
public final class Period implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
public Period(Date start, Date end) {
// Constructor enforces invariant:
if (start.after(end)) throw new IllegalArgumentException("start after end");
this.start = new Date(start.getTime()); // defensive copy
this.end = new Date(end.getTime());
}
// writeReplace: instead of serializing 'this', serialize the proxy
private Object writeReplace() {
return new SerializationProxy(this);
}
// readObject: prevent direct deserialization of Period instances
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Use serialization proxy");
}
// Private static proxy class — minimal, correct state
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
// readResolve: reconstruct Period through its public constructor
private Object readResolve() {
return new Period(start, end); // invariant enforced — no way to bypass
}
}
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }
}
// ── Modern alternative: Jackson JSON serialization ─────────────────────
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
// Serialize to JSON byte array:
byte[] json = mapper.writeValueAsBytes(alice);
System.out.println(new String(json));
// {"name":"Alice","age":30,"email":"alice@example.com"}
// Deserialize from JSON:
Person restored = mapper.readValue(json, Person.class);
// No security risk from untrusted sources (no code execution via gadget chains)
// Schema evolution: add fields freely — missing fields get defaults, extra fields ignored
// ── writeReplace / readResolve for singleton pattern ─────────────────
public class Config implements Serializable {
private static final long serialVersionUID = 1L;
private static final Config INSTANCE = new Config();
private Config() {}
public static Config getInstance() { return INSTANCE; }
// Preserve singleton property across serialization:
private Object readResolve() {
return INSTANCE; // replace deserialized instance with the singleton
}
}
Config c1 = Config.getInstance();
byte[] bytes = serialize(c1); // serialize
Config c2 = (Config) deserialize(bytes); // deserialize
System.out.println(c1 == c2); // true — readResolve ensures singleton