Serialization & Deserialization with Immutable Classes

Serialization is the process of converting an object into a byte stream, and deserialization is reconstructing the object back from the stream.

For immutable classes, special care is needed because:

  1. Fields are declared final.
  2. We rely on deep copy and defensive copies to ensure immutability.
  3. The Java deserialization mechanism bypasses the constructor — meaning your deep copy logic in the constructor won’t automatically run during deserialization.

✅ Problem Scenario

Imagine you have an immutable class with a mutable field (Date).

import java.io.Serializable;
import java.util.Date;

public final class Employee implements Serializable {
    private static final long serialVersionUID = 1L;

    private final String name;
    private final Date joiningDate; // mutable field

    public Employee(String name, Date joiningDate) {
        this.name = name;
        this.joiningDate = new Date(joiningDate.getTime()); // defensive copy
    }

    public String getName() {
        return name;
    }

    public Date getJoiningDate() {
        return new Date(joiningDate.getTime()); // defensive copy
    }
}

👉 During normal construction, joiningDate is safely deep-copied.
👉 But during deserialization, Java restores fields directly from the stream, bypassing the constructor — so no defensive copy is made.

This means the deserialized object might hold a direct reference to the original mutable object stored in the stream, breaking immutability.

✅ Solution: Custom readObject Method

To maintain immutability, override the readObject method and re-apply defensive copying:

private void readObject(java.io.ObjectInputStream in)
        throws java.io.IOException, ClassNotFoundException {
    in.defaultReadObject();

    // Defensive copy after deserialization
    if (joiningDate != null) {
        // Reflection sets final field, so we must ensure immutability manually
        java.lang.reflect.Field field;
        try {
            field = Employee.class.getDeclaredField("joiningDate");
            field.setAccessible(true);
            field.set(this, new Date(joiningDate.getTime())); // deep copy
        } catch (Exception e) {
            throw new java.io.InvalidObjectException("Failed to maintain immutability");
        }
    }
}

✅ Now even after deserialization, the joiningDate field is safely deep-copied.

✅ Alternative: readResolve Method

Another technique is using readResolve, which allows you to replace the deserialized object with a properly constructed one:

private Object readResolve() {
    // Recreate immutable object using constructor
    return new Employee(this.name, this.joiningDate);
}

✅ This ensures your constructor logic (including deep copy) is applied.

⚠️ Things to Be Careful About

  1. Final Fields and Reflection
    • Java deserialization sets final fields via reflection, so ensure your custom logic (readObject or readResolve) maintains immutability.
  2. Serialization Proxy Pattern (Best Practice)
    • Instead of allowing direct serialization of your immutable class, use a proxy object that handles serialization safely.
    Example:
private Object writeReplace() {
    return new SerializationProxy(this);
}

private Object readResolve() {
    throw new java.io.InvalidObjectException("Proxy required");
}

private static class SerializationProxy implements Serializable {
    private final String name;
    private final Date joiningDate;

    SerializationProxy(Employee e) {
        this.name = e.name;
        this.joiningDate = e.joiningDate;
    }

    private Object readResolve() {
        return new Employee(name, joiningDate); // constructor ensures immutability
    }
}

✅ This is the safest approach for immutable classes.

🔑 Summary

  • Issue: Deserialization bypasses constructor, so deep copy logic isn’t applied.
  • Solution:
    • Use readObject to manually enforce immutability.
    • Or use readResolve to reconstruct via constructor.
    • Best practice: adopt Serialization Proxy Pattern for maximum safety.

Backend developer working with Java, Spring Boot, Microservices, NoSQL, and AWS. I love sharing knowledge, practical tips, and clean code practices to help others build scalable applications.

Leave a Reply

Your email address will not be published. Required fields are marked *