1. Méthodes writeReplace et readResolve : théorie
Parfois, les mécanismes standard de sérialisation ne suffisent pas. Imaginez la situation suivante : vous avez un singleton (une classe qui ne peut avoir qu’une seule instance dans tout le programme) et vous souhaitez qu’après la désérialisation il reste l’unique instance (qu’aucun nouveau clone n’apparaisse). Ou vous voulez sérialiser non pas l’objet lui‑même, mais une version « allégée » (proxy) afin de cacher des détails d’implémentation ou d’économiser de l’espace.
En Java, il existe pour cela des méthodes spéciales : writeReplace et readResolve. Leur rôle est de remplacer l’objet en cours de sérialisation ou de désérialisation par un autre.
Une analogie simple :
C’est comme si vous envoyiez un colis à un ami, mais qu’au lieu de vous‑même vous mettiez dans la boîte un double en jouet. Et quand l’ami ouvre le colis, la figurine se transforme dans ses mains en vous — le vrai ! (Dans la vie réelle, cela ne marche pas ainsi, mais en Java — tout à fait.)
writeReplace
La méthode private Object writeReplace() est appelée sur l’objet avant la sérialisation. Elle peut renvoyer n’importe quel objet qui sera effectivement sérialisé à la place de l’original. Si elle n’est pas implémentée, c’est l’objet lui‑même qui est sérialisé.
Signature :
private Object writeReplace() throws ObjectStreamException
readResolve
La méthode private Object readResolve() est appelée sur l’objet après la désérialisation. Elle permet de remplacer l’objet qui vient d’être créé par un autre (par exemple renvoyer un singleton ou une instance issue d’un cache).
Signature :
private Object readResolve() throws ObjectStreamException
Important :
Les deux méthodes doivent être private et renvoyer Object. C’est une exigence de la spécification de sérialisation Java. Si vous les déclarez public, la sérialisation les ignorera tout simplement.
2. Mise en pratique de writeReplace et readResolve
Singleton et readResolve
Un singleton est simplement une classe qui ne peut avoir qu’une seule instance dans tout le programme. Si un tel objet est sérialisé puis restauré, sans la méthode readResolve une nouvelle instance apparaîtra et la règle d’unicité sera violée. Avec readResolve, on peut renvoyer exactement le même objet, en conservant l’idée du singleton.
import java.io.*;
public class MySingleton implements Serializable {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
// Nous garantissons qu'après la désérialisation c'est bien INSTANCE qui sera renvoyé
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
Explication :
Sans readResolve, après la désérialisation un nouvel objet apparaîtra, différent (par ==) du singleton original. Avec readResolve, c’est toujours INSTANCE qui est renvoyé.
Vérifions en pratique :
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 readResolve est présent ; false — sans lui
writeReplace : sérialisation d’un objet proxy
Parfois, un objet est trop lourd à sérialiser, contient des données sensibles, ou ne doit simplement pas sortir dans son intégralité. Dans ce cas, on peut sérialiser un « remplaçant » — un objet proxy.
Exemple :
Supposons que nous ayons une classe User avec un mot de passe privé. Nous ne voulons pas que le mot de passe soit sérialisé.
import java.io.*;
public class User implements Serializable {
private String username;
private transient String password; // transient — n'est pas sérialisé
public User(String username, String password) {
this.username = username;
this.password = password;
}
// À la place de User, nous sérialisons uniquement UserProxy
private Object writeReplace() throws ObjectStreamException {
return new UserProxy(username);
}
// Classe proxy — uniquement pour la sérialisation
private static class UserProxy implements Serializable {
private String username;
public UserProxy(String username) {
this.username = username;
}
private Object readResolve() throws ObjectStreamException {
// En pratique, on ne peut pas reconstruire le mot de passe — on renvoie un User avec un mot de passe vide
return new User(username, "");
}
}
}
Explication :
- Lors de la sérialisation, User est transformé en UserProxy (sans mot de passe).
- Lors de la désérialisation, UserProxy est transformé de nouveau en User (mais le mot de passe est vide).
3. Personnaliser la sérialisation pour les objets immuables
Les objets immuables (non modifiables) utilisent souvent des champs final privés et n’ont pas de setters. La sérialisation standard de Java peut contourner cette contrainte, mais il est parfois préférable de contrôler explicitement le processus via writeReplace/readResolve.
Exemple : 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);
}
}
}
Explication :
- Lors de la sérialisation, Money est transformé en MoneyProxy (POJO).
- Lors de la désérialisation, MoneyProxy est transformé de nouveau en Money.
Interaction avec writeObject/readObject
Les méthodes writeReplace/readResolve fonctionnent indépendamment de writeObject/readObject. Si les deux mécanismes sont définis, writeReplace est appelé d’abord, puis sur l’objet renvoyé — writeObject (s’il implémente Serializable).
Schéma :
flowchart LR
A[Objet] -- writeReplace --> B[Objet proxy]
B -- writeObject --> C[Flux d'octets]
C -- readObject --> D[Objet proxy]
D -- readResolve --> E[Objet final]
4. Pratique : sérialisation avec substitution d’objet
Ajoutons une sérialisation personnalisée à votre application d’apprentissage — par exemple pour la classe Person, de sorte qu’à la sérialisation seul le nom soit enregistré et que l’âge soit ignoré (admettons que nous nous soucions de la confidentialité).
Étape 1. Classe principale
import java.io.*;
public class Person implements Serializable {
private String name;
private int age; // nous ne voulons pas le sérialiser
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 — « âge inconnu »
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Étape 2. Test
public class TestCustomSerialization {
public static void main(String[] args) throws Exception {
Person original = new Person("Alice", 30);
// Sérialisation
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
out.writeObject(original);
out.close();
// Désérialisation
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
Person deserialized = (Person) in.readObject();
in.close();
System.out.println("Avant la sérialisation: " + original);
System.out.println("Après la désérialisation: " + deserialized);
}
}
Résultat :
Avant la sérialisation: Person{name='Alice', age=30}
Après la désérialisation: Person{name='Alice', age=-1}
Comme vous le voyez, l’âge n’a pas été sérialisé — tout se passe comme prévu !
5. Particularités et nuances
Quand utiliser writeReplace/readResolve ?
- Lorsque vous devez sérialiser uniquement une partie de l’état de l’objet.
- Pour la sérialisation/désérialisation d’objets proxy.
- Pour prendre en charge le pattern Singleton.
- Pour les objets immuables ou complexes dont la structure interne peut évoluer.
Quand ne pas l’utiliser ?
- S’il suffit d’utiliser des champs transient ou writeObject/readObject.
- Si l’objet ne doit pas être remplacé par un autre.
Compatibilité avec l’héritage
Si la superclasse définit writeReplace/readResolve, ils seront appelés aussi pour les sous-classes (s’ils ne sont pas redéfinis). Soyez prudent avec les hiérarchies !
6. Erreurs typiques lors d’une sérialisation personnalisée
Erreur n° 1 : visibilité incorrecte des méthodes. Si vous déclarez writeReplace/readResolve autrement que private, la sérialisation ne les appellera pas. Uniquement private !
Erreur n° 2 : incohérence des types de retour. writeReplace/readResolve doivent renvoyer un Object. Même si vous retournez en pratique votre type, déclarez le type de retour comme Object.
Erreur n° 3 : perte de données. Si le proxy ne contient pas toutes les données nécessaires pour reconstruire l’objet initial, une partie de l’information sera perdue. Vérifiez toujours que vous pourrez recréer l’objet.
Erreur n° 4 : violation des invariants. readResolve doit renvoyer un objet conforme aux attentes du programme (par exemple, pour un singleton — précisément INSTANCE).
Erreur n° 5 : exceptions non gérées. writeReplace/readResolve peuvent lever ObjectStreamException. Gérez‑la ou propagez‑la explicitement.
GO TO FULL VERSION