1. Métodos writeReplace y readResolve: teoría
A veces los mecanismos estándar de serialización no son suficientes. Imagine que tiene un singleton (una clase que solo puede tener una instancia en toda la aplicación) y quiere que, tras la deserialización, siga siendo la única instancia (y no aparezca un clon nuevo). O quizá quiere serializar no el objeto en sí, sino su versión «ligera» (proxy), para ocultar detalles de implementación o ahorrar espacio.
En Java existen métodos especiales para esto: writeReplace y readResolve. Su tarea es sustituir el objeto serializable o deserializable por otro.
Analogía sencilla:
Es como si fuera a enviar un paquete a un amigo, pero en lugar de usted, metiera dentro un doble de juguete. Y cuando su amigo abre el paquete, en lugar del juguete aparece usted —¡el verdadero! (En la vida real no funciona así, pero en Java sí).
writeReplace
El método private Object writeReplace() se invoca en el objeto antes de la serialización. Puede devolver cualquier objeto, que será el que realmente se serialice en lugar del original. Si no se implementa, se serializa el propio objeto.
Firma:
private Object writeReplace() throws ObjectStreamException
readResolve
El método private Object readResolve() se invoca en el objeto después de la deserialización. Permite sustituir el objeto recién creado por otro (por ejemplo, devolver el singleton o una instancia en caché).
Firma:
private Object readResolve() throws ObjectStreamException
Importante:
Ambos métodos deben ser private y devolver Object. Es un requisito de la especificación de serialización de Java. Si los hace public, la serialización simplemente los ignorará.
2. Aplicación de writeReplace y readResolve en la práctica
Singleton y readResolve
Un singleton es simplemente una clase que solo puede tener una instancia en toda la aplicación. Si se serializa dicho objeto y luego se restaura, sin el método readResolve aparecerá una instancia nueva, y se romperá la regla de la «unicidad». Con readResolve puede devolverse exactamente ese mismo objeto, preservando la idea del singleton.
import java.io.*;
public class MySingleton implements Serializable {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
// Garantizamos que tras la deserialización se devolverá exactamente INSTANCE
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
Explicación:
Sin readResolve, tras la deserialización aparecerá un nuevo objeto que no es igual (por ==) al singleton original. Con readResolve siempre se devuelve INSTANCE.
Probemos en la práctica:
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 si existe readResolve; false - sin él
writeReplace: serialización de un objeto proxy
A veces un objeto es demasiado pesado para serializar, contiene datos sensibles o simplemente no debería exponerse por completo. En ese caso, puede serializarse un «sustituto»: un objeto proxy.
Ejemplo:
Supongamos que tenemos la clase User con una contraseña privada. No queremos que la contraseña se serialice.
import java.io.*;
public class User implements Serializable {
private String username;
private transient String password; // transient - no se serializa
public User(String username, String password) {
this.username = username;
this.password = password;
}
// En lugar de User serializamos solo UserProxy
private Object writeReplace() throws ObjectStreamException {
return new UserProxy(username);
}
// Clase proxy - solo para serialización
private static class UserProxy implements Serializable {
private String username;
public UserProxy(String username) {
this.username = username;
}
private Object readResolve() throws ObjectStreamException {
// En la vida real la contraseña no se puede recuperar - devolvemos User con contraseña vacía
return new User(username, "");
}
}
}
Explicación:
- Al serializar, User se convierte en UserProxy (sin contraseña).
- Al deserializar, UserProxy se convierte de nuevo en User (pero la contraseña ya está vacía).
3. Personalización de la serialización para objetos inmutables
Los objetos inmutables suelen usar campos final privados y no tienen setters. Con la serialización estándar de Java, esto se puede salvar, pero a veces es mejor controlar explícitamente el proceso mediante writeReplace/readResolve.
Ejemplo: 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);
}
}
}
Explicación:
- Al serializar, Money se convierte en MoneyProxy (POJO).
- Al deserializar, MoneyProxy se convierte de nuevo en Money.
Interacción con writeObject/readObject
Los métodos writeReplace/readResolve funcionan de forma independiente de writeObject/readObject. Si ambos mecanismos están definidos, primero se invoca writeReplace y, sobre el objeto devuelto, writeObject (si implementa Serializable).
Esquema:
flowchart LR
A[Objeto] -- writeReplace --> B[Objeto proxy]
B -- writeObject --> C[Flujo de bytes]
C -- readObject --> D[Objeto proxy]
D -- readResolve --> E[Objeto final]
4. Práctica: serialización con sustitución de objeto
Añadamos una serialización personalizada a su aplicación educativa —por ejemplo, para la clase Person, de modo que al serializar se guarde solo el nombre y la edad se ignore (supongamos que nos preocupa la privacidad).
Paso 1. Clase principal
import java.io.*;
public class Person implements Serializable {
private String name;
private int age; // no queremos serializarlo
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 - "edad desconocida"
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Paso 2. Probamos
public class TestCustomSerialization {
public static void main(String[] args) throws Exception {
Person original = new Person("Alice", 30);
// Serialización
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
out.writeObject(original);
out.close();
// Deserialización
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
Person deserialized = (Person) in.readObject();
in.close();
System.out.println("Antes de la serialización: " + original);
System.out.println("Después de la deserialización: " + deserialized);
}
}
Resultado:
Antes de la serialización: Person{name='Alice', age=30}
Después de la deserialización: Person{name='Alice', age=-1}
Como ve, la edad no se serializó — todo según lo previsto.
5. Particularidades y matices
¿Cuándo usar writeReplace/readResolve?
- Cuando se necesita serializar solo parte del estado del objeto.
- Para la serialización/deserialización de objetos proxy.
- Para soportar el patrón Singleton.
- Para objetos inmutables o complejos cuya estructura interna puede cambiar.
¿Cuándo no conviene usarlos?
- Si basta con campos transient o con writeObject/readObject.
- Si el objeto no debe ser sustituido por otro.
Compatibilidad con la herencia
Si la superclase define writeReplace/readResolve, se llamarán también para las subclases (si no se sobrescriben). ¡Sea cuidadoso con las jerarquías!
6. Errores típicos en la serialización personalizada
Error n.º 1: visibilidad incorrecta de los métodos. Si hace writeReplace/readResolve que no sean private, la serialización no los invocará. ¡Solo private!
Error n.º 2: discrepancia en los tipos de retorno. writeReplace/readResolve deben devolver Object. Incluso si en realidad devuelve su propio tipo, declare el método con tipo de retorno Object.
Error n.º 3: pérdida de datos. Si el objeto proxy no contiene todos los datos necesarios para restaurar el objeto original, se perderá parte de la información. Verifique siempre que podrá reconstruir el objeto.
Error n.º 4: violación de invariantes. readResolve debe devolver un objeto que cumpla las expectativas del programa (por ejemplo, para un singleton — precisamente INSTANCE).
Error n.º 5: excepciones sin manejar. writeReplace/readResolve pueden lanzar ObjectStreamException. Manéjela o propáguela explícitamente.
GO TO FULL VERSION