Serialization
Serialization is the process of converting a Java object into a stream of bytes so it can be saved to a file, sent over a network, or stored in a database — and then reconstructed (deserialized) back into an identical object later. It is one of Java’s oldest persistence mechanisms and still widely used in caching systems, RMI, and distributed computing.
Why Serialization Matters
Imagine you have a User object with a name, email, and preferences. Without serialization, that object only exists in memory for the lifetime of your program. Serialization lets you:
- Persist objects to disk and reload them later
- Transmit objects across a network (e.g., RMI, sockets)
- Cache complex object graphs and restore them quickly
- Deep-copy objects by serializing and immediately deserializing them
Making a Class Serializable
To make a class serializable, implement the java.io.Serializable interface. It is a marker interface — it has no methods. You are simply telling the JVM “this class is safe to serialize.”
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String email;
private int age;
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
@Override
public String toString() {
return "User{name='" + name + "', email='" + email + "', age=" + age + "}";
}
}
Note: Always declare a
private static final long serialVersionUID. If you don’t, the JVM generates one automatically based on the class structure — and any change to the class (adding a field, changing a method) will generate a different UID, causingInvalidClassExceptionwhen you try to deserialize old data.
Serializing an Object
Use ObjectOutputStream wrapped around a FileOutputStream (or any OutputStream) to write an object:
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) throws IOException {
User user = new User("Alice", "[email protected]", 30);
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Object serialized: " + user);
}
}
}
Output:
Object serialized: User{name='Alice', email='[email protected]', age=30}
The file user.ser now contains the binary representation of the User object.
Deserializing an Object
Use ObjectInputStream to read the object back. The class must be on the classpath at deserialization time:
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("Object deserialized: " + user);
}
}
}
Output:
Object deserialized: User{name='Alice', email='[email protected]', age=30}
Tip: Always use try-with-resources when working with
ObjectOutputStreamandObjectInputStream— they implementCloseableand must be properly closed to flush the data and release file handles.
serialVersionUID Explained
The serialVersionUID is a version stamp embedded in the serialized byte stream. When deserializing, Java checks that the UID in the stream matches the UID of the loaded class. If they don’t match, deserialization fails with InvalidClassException.
// Explicit UID — stable across changes you consider backward-compatible
private static final long serialVersionUID = 42L;
| Scenario | Result |
|---|---|
| Same class, same UID | Deserialization succeeds |
| Class changed, UID unchanged (your choice) | Succeeds; new fields get default values |
| Class changed, UID auto-regenerated by JVM | InvalidClassException — stream UID mismatch |
No serialVersionUID declared | JVM computes it — fragile, avoid this |
Warning: If you rely on serialized data surviving across deployments (files on disk, network caches), always declare an explicit
serialVersionUIDand increment it intentionally when making breaking changes.
What Gets Serialized?
Serialization captures the instance state — the values of all non-static, non-transient fields. Here is what is included and excluded:
| Included | Excluded |
|---|---|
| Instance fields of the object | static fields (class-level, not instance state) |
Fields of superclasses (if also Serializable) | transient fields (explicitly excluded) |
Nested/referenced objects (if also Serializable) | Methods and constructors |
If any field references an object whose class does not implement Serializable, you will get a NotSerializableException at runtime.
The transient Keyword
Mark a field transient to exclude it from serialization. This is useful for passwords, open file handles, cached computed values, or anything that should not leave the JVM:
public class Session implements Serializable {
private static final long serialVersionUID = 1L;
private String sessionId;
private transient String cachedToken; // NOT serialized
private transient java.sql.Connection dbConnection; // NOT serialized
public Session(String sessionId, String cachedToken) {
this.sessionId = sessionId;
this.cachedToken = cachedToken;
}
@Override
public String toString() {
return "Session{id='" + sessionId + "', token='" + cachedToken + "'}";
}
}
When deserialized, cachedToken will be null (or 0 / false for primitives). See the transient Keyword page for full details and patterns for reinitializing transient fields after deserialization.
Lifecycle Hooks: readObject and writeObject
You can customize serialization behavior by declaring private writeObject and readObject methods in your class. The JVM calls them automatically:
import java.io.*;
public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // never serialize raw password
public SecureUser(String name, String password) {
this.name = name;
this.password = password;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // serialize non-transient fields normally
// write a hashed version instead of the raw password
oos.writeObject(hash(password));
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // deserialize non-transient fields
String hashedPassword = (String) ois.readObject();
// restore transient field from the hashed value
this.password = "[hashed:" + hashedPassword + "]";
}
private String hash(String input) {
// simplified — use BCrypt or SHA-256 in real code
return Integer.toHexString(input.hashCode());
}
}
Tip:
writeReplace()andreadResolve()are two more lifecycle hooks.readResolve()is commonly used in singleton patterns to ensure deserialization returns the existing singleton instance rather than creating a new one.
Serializing Object Graphs
Serialization is smart about object graphs. If two fields reference the same object, Java serializes it only once and restores the reference correctly on deserialization:
User alice = new User("Alice", "[email protected]", 30);
List<User> team = new ArrayList<>();
team.add(alice);
team.add(alice); // same reference twice
After round-tripping through serialization, both entries in team will still point to the same User object. This also means that circular references (A references B, B references A) are handled without infinite loops.
Under the Hood
When you call oos.writeObject(obj), the JVM:
- Checks that the object’s class implements
Serializable(orExternalizable). ThrowsNotSerializableExceptionif not. - Assigns a handle to the object. If the same object appears again later in the stream, only the handle is written (no duplication).
- Writes class metadata — the fully qualified class name,
serialVersionUID, and field descriptors — into the stream header. - Recursively serializes each non-static, non-transient field, following the object graph.
- Calls
writeObjectif defined, allowing custom serialization logic.
On deserialization, readObject reverses this: reads the class descriptor, verifies the UID, allocates the object without calling any constructor, then restores field values. The fact that no constructor is called is important — your constructor validation logic does not run during deserialization.
The binary format is described in the Java Object Serialization Specification. Each serialized file starts with the magic bytes 0xACED 0x0005.
Security Considerations
Deserialization is a well-known attack surface. Never deserialize data from an untrusted source without validation:
- An attacker can craft a malicious byte stream that triggers arbitrary code execution through gadget chains in your classpath.
- Java 9+ introduced a serialization filter mechanism (
ObjectInputFilter) that lets you whitelist or blacklist classes during deserialization.
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(info -> {
if (info.serialClass() != null &&
!info.serialClass().getName().startsWith("com.myapp.")) {
return ObjectInputFilter.Status.REJECTED;
}
return ObjectInputFilter.Status.ALLOWED;
});
Warning: Deserializing data from untrusted sources (HTTP requests, user-uploaded files, external queues) without a serialization filter is a critical security vulnerability. Prefer JSON/XML serialization libraries (Jackson, Gson) for data exchange with external systems.
Externalizable: Full Control
If you need complete control over the serialization format and performance matters (e.g., you are serializing millions of objects), implement Externalizable instead of Serializable:
import java.io.*;
public class Point implements Externalizable {
private int x, y;
// Required public no-arg constructor
public Point() {}
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(x);
out.writeInt(y);
}
@Override
public void readExternal(ObjectInput in) throws IOException {
x = in.readInt();
y = in.readInt();
}
}
| Feature | Serializable | Externalizable |
|---|---|---|
| Implementation effort | Low — marker interface | High — must implement read/write |
| Control over format | Limited (hooks only) | Complete |
| Performance | Moderate | Can be very fast |
| Constructor called on deserialization | No | Yes (no-arg constructor) |
| Default field handling | Automatic | Manual |
In This Section
- transient Keyword — exclude sensitive or non-serializable fields from the byte stream and learn patterns for reinitializing them after deserialization.
Related Topics
- Object Streams — the
ObjectInputStreamandObjectOutputStreamclasses that power serialization under the hood. - transient Keyword — control exactly which fields are excluded from serialization.
- File Handling — persist serialized byte streams to disk using Java’s file I/O APIs.
- Java I/O — the broader I/O framework that serialization streams plug into.
- RMI — Remote Method Invocation, which uses serialization to pass objects between JVMs over a network.
- NIO.2: Path & Files — a modern alternative for file operations alongside classic serialization workflows.