CodeGym /Cours /JAVA 25 SELF /Configuration du comportement de la sérialisation : métho...

Configuration du comportement de la sérialisation : méthodes personnalisées

JAVA 25 SELF
Niveau 43 , Leçon 3
Disponible

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.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION