1. writeReplace and readResolve methods: theory
Sometimes the standard serialization tools are not enough. Imagine this situation: you have a singleton (a class that can have only one instance in the entire program), and you want it to remain the only instance after deserialization (instead of a new clone appearing). Or you want to serialize not the object itself, but its “lightweight” version (a proxy) to hide implementation details or save space.
Java provides special methods for this: writeReplace and readResolve. Their job is to replace the object being serialized or deserialized with another one.
A simple analogy:
It’s like sending a package to a friend, but instead of yourself, you put a toy double in the box. And when your friend unpacks the package, they end up holding you—the real one! (In real life, that doesn’t work, but in Java—it does.)
writeReplace
The method private Object writeReplace() is called on the object before serialization. It can return any object, which will be serialized instead of the original. If it is not implemented, the object itself is serialized.
Signature:
private Object writeReplace() throws ObjectStreamException
readResolve
The method private Object readResolve() is called on the object after deserialization. It allows you to replace the newly created object with another one (for example, return a singleton or a cached instance).
Signature:
private Object readResolve() throws ObjectStreamException
Important:
Both methods must be private and return Object. This is required by the Java serialization specification. If you make them public, serialization will simply ignore them.
2. Using writeReplace and readResolve in practice
Singleton and readResolve
A singleton is simply a class that can have only one instance in the entire program. If you serialize and then restore such an object, without readResolve a new instance will appear, breaking the “single instance” rule. With readResolve you can return the very same object, preserving the singleton idea.
import java.io.*;
public class MySingleton implements Serializable {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
// Ensure that after deserialization exactly INSTANCE is returned
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
Explanation:
Without readResolve, after deserialization a new object will appear that is not the same (by ==) as the original singleton. With readResolve, INSTANCE is always returned.
Let’s test it:
MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();
System.out.println(s1 == s2); // true if readResolve is present; false without it
writeReplace: serializing a proxy object
Sometimes an object is too heavy for serialization, contains sensitive data, or simply should not be exposed in full. In this case, you can serialize a “replacement”—a proxy object.
Example:
Suppose we have a User class with a private password. We don’t want the password to be serialized.
import java.io.*;
public class User implements Serializable {
private String username;
private transient String password; // transient: not serialized
public User(String username, String password) {
this.username = username;
this.password = password;
}
// Instead of User, serialize only UserProxy
private Object writeReplace() throws ObjectStreamException {
return new UserProxy(username);
}
// Proxy class — for serialization only
private static class UserProxy implements Serializable {
private String username;
public UserProxy(String username) {
this.username = username;
}
private Object readResolve() throws ObjectStreamException {
// In real life the password cannot be restored, so we return a User with an empty password
return new User(username, "");
}
}
}
Explanation:
- During serialization, User turns into UserProxy (without a password).
- During deserialization, UserProxy turns back into User (but the password is empty).
3. Customizing serialization for immutable objects
Immutable objects often use private final fields and have no setters. With standard Java serialization this restriction can be bypassed, but sometimes it is better to explicitly control the process using writeReplace/readResolve.
Example: Value Object
import java.io.*;
public final class Money implements Serializable {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object writeReplace() throws ObjectStreamException {
return new MoneyProxy(amount, currency);
}
private static class MoneyProxy implements Serializable {
private final int amount;
private final String currency;
MoneyProxy(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object readResolve() throws ObjectStreamException {
return new Money(amount, currency);
}
}
}
Explanation:
- During serialization, Money becomes MoneyProxy (a POJO).
- During deserialization, MoneyProxy turns back into Money.
Interaction with writeObject/readObject
The methods writeReplace/readResolve work independently of writeObject/readObject. If both mechanisms are defined, then writeReplace is called first, and on the returned object—writeObject (if it implements Serializable).
Diagram:
flowchart LR
A[Object] -- writeReplace --> B[Proxy object]
B -- writeObject --> C[Byte stream]
C -- readObject --> D[Proxy object]
D -- readResolve --> E[Final object]
4. Practice: serialization with object replacement
Let’s add custom serialization to your training application—for example, for the Person class, so that only the name is written during serialization, and the age is ignored (let’s say we care about privacy).
Step 1. Main class
import java.io.*;
public class Person implements Serializable {
private String name;
private int age; // we don't want to serialize
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() throws ObjectStreamException {
return new PersonProxy(name);
}
private static class PersonProxy implements Serializable {
private final String name;
PersonProxy(String name) {
this.name = name;
}
private Object readResolve() throws ObjectStreamException {
return new Person(name, -1); // -1 - "age unknown"
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Step 2. Test
public class TestCustomSerialization {
public static void main(String[] args) throws Exception {
Person original = new Person("Alice", 30);
// Serialization
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
out.writeObject(original);
out.close();
// Deserialization
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
Person deserialized = (Person) in.readObject();
in.close();
System.out.println("Before serialization: " + original);
System.out.println("After deserialization: " + deserialized);
}
}
Result:
Before serialization: Person{name='Alice', age=30}
After deserialization: Person{name='Alice', age=-1}
As you can see, the age was not serialized—everything is going according to plan!
5. Details and nuances
When to use writeReplace/readResolve?
- When you need to serialize only part of an object’s state.
- For serializing/deserializing proxy objects.
- To support the Singleton pattern.
- For immutable or complex objects whose internal structure may change.
When should you avoid it?
- If you can make do with transient fields or writeObject/readObject.
- If the object should not be replaced by another.
Compatibility with inheritance
If a superclass defines writeReplace/readResolve, they will be called for subclasses as well (unless overridden). Be careful with hierarchies!
6. Common mistakes in custom serialization
Mistake No. 1: Incorrect method visibility. If you make writeReplace/readResolve not private, serialization won’t call them. Only private!
Mistake No. 2: Return type mismatch. writeReplace/readResolve must return Object. Even if you actually return your own type, declare the method with the return type Object.
Mistake No. 3: Data loss. If the proxy object does not contain all the data needed to reconstruct the original object, some information will be lost. Always ensure you can restore the object back.
Mistake No. 4: Violating invariants. readResolve must return an object that matches the program’s expectations (for example, for a singleton—exactly INSTANCE).
Mistake No. 5: Unhandled exceptions. writeReplace/readResolve may throw ObjectStreamException. Handle it or declare it explicitly.
GO TO FULL VERSION