CodeGym /Courses /JAVA 25 SELF /Configuring serialization behavior: custom methods

Configuring serialization behavior: custom methods

JAVA 25 SELF
Level 43, Lesson 3
Available

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.

1
Task
JAVA 25 SELF, level 43, lesson 3
Locked
Cosmic Nexus: Ensuring Unity of Existence
Cosmic Nexus: Ensuring Unity of Existence
1
Task
JAVA 25 SELF, level 43, lesson 3
Locked
Secret Data Storage: Masking Information During Transfer
Secret Data Storage: Masking Information During Transfer
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION