1. Introduction
La sérialisation automatique, c’est comme le pilote automatique d’un avion : elle fonctionne très bien tant que tout se passe comme prévu. Mais dès que surviennent des conditions particulières, on se rend compte que le mécanisme simple ne suffit plus. Imaginez que vous deviez enregistrer un objet, mais pas tous ses champs : certaines données sont temporaires, d’autres trop sensibles pour être écrites dans un fichier. À l’inverse, lors de l’enregistrement, il peut être nécessaire d’ajouter quelque chose qui vous est propre, par exemple une version ou une somme de contrôle. Il arrive aussi qu’avant d’écrire ou de charger des données, il faille effectuer une validation ou une transformation. Et parfois la tâche est encore plus complexe : assurer la compatibilité avec les versions précédentes de la classe si sa structure a évolué dans le temps.
Dans de telles situations, on comprend qu’une sérialisation standard ne suffit pas. Il faut reprendre la main.
Méthodes spéciales de sérialisation : writeObject et readObject
En Java, deux méthodes spéciales permettent de contrôler entièrement la sérialisation et la désérialisation d’un objet :
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Important !
- Les méthodes doivent être exactement private (ni public, ni protected, ni package-private).
- Les signatures doivent correspondre exactement à celles ci-dessus.
- Si ces méthodes sont déclarées dans votre classe, elles seront appelées à la place de la sérialisation/désérialisation standard.
Comment cela fonctionne-t-il ?
Lorsque vous appelez ObjectOutputStream.writeObject(obj), la JVM cherche d’abord dans la classe de obj la méthode private void writeObject(ObjectOutputStream). Si elle existe — c’est elle qui est appelée. De même, à la désérialisation, private void readObject(ObjectInputStream) est appelée.
Si les méthodes ne sont pas déclarées, la sérialisation standard est utilisée.
Structure de writeObject et readObject
Signatures des méthodes
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
À l’intérieur de ces méthodes, vous devez obligatoirement appeler :
- out.defaultWriteObject(); — pour sérialiser les champs standard (non transient) de la superclasse et de la classe courante.
- in.defaultReadObject(); — pour désérialiser les champs standard.
Si vous ne les appelez pas, les champs standard ne seront pas sérialisés — et à la désérialisation l’objet se retrouvera « vide ». C’est comme oublier de mettre votre passeport dans la valise : formellement vous êtes arrivé, mais vous ne pourrez pas prouver qui vous êtes.
2. Exemple : ajouter une somme de contrôle lors de la sérialisation
Examinons un exemple pratique. Supposons que nous ayons une classe utilisateur et que nous souhaitions, lors de la sérialisation, ajouter à l’objet une somme de contrôle afin de vérifier l’intégrité des données à la désérialisation.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// champ transient — il n'est pas sérialisé
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Sérialisation personnalisée
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // On enregistre les champs standard
int sum = calculateChecksum();
out.writeInt(sum); // On écrit la somme de contrôle
System.out.println("[LOG] Sérialisation User: checksum=" + sum);
}
// Désérialisation personnalisée
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // On restaure les champs standard
int sum = in.readInt(); // On lit la somme de contrôle
int actual = calculateChecksum();
System.out.println("[LOG] Désérialisation User: checksum=" + sum + ", actuelle=" + actual);
if (sum != actual) {
throw new IOException("Données corrompues ! La somme de contrôle ne correspond pas.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Exemple d’utilisation :
// On enregistre l'objet
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// On charge l'objet
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Objet restauré : " + loaded);
}
Que se passe-t-il ?
- Lors de la sérialisation, writeObject est appelée : les champs standard sont enregistrés, plus la somme de contrôle.
- Lors de la désérialisation, readObject est appelée : les champs sont restaurés et la somme de contrôle est vérifiée.
- Un journal s’affichera dans la console et, en cas de problème, une exception sera levée.
3. Exclure les données sensibles de la sérialisation
Parfois, certains champs ne doivent pas être sérialisés (par exemple, des mots de passe). Pour cela, on peut utiliser le mot-clé transient (nous y reviendrons dans la prochaine leçon), mais on peut aussi éviter de sérialiser le champ manuellement si vous implémentez writeObject.
Exemple :
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient : non sérialisé
// Mais on peut aussi faire ainsi :
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Ne pas écrire password !
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password reste null
}
}
Attention :
Si vous souhaitez ne sérialiser qu’une partie de l’objet, n’écrivez tout simplement pas les champs superflus dans le flux.
4. Appel des méthodes de la superclasse : defaultWriteObject et defaultReadObject
À l’intérieur de vos méthodes writeObject et readObject, il faut presque toujours appeler defaultWriteObject() et defaultReadObject(). C’est comme appuyer sur « enregistrer le brouillon » avant d’ajouter vos propres notes.
Ces méthodes se chargent de la sérialisation standard de tous les champs non-transient, non-static de la classe courante et de la superclasse. Si vous ne les appelez pas, ces champs ne seront pas sérialisés et seront vides à la désérialisation.
Exemple de mauvais comportement :
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // oublié de l'appeler !
out.writeInt(123); // quelque chose de spécifique
}
Dans ce cas, les champs standard ne seront tout simplement pas enregistrés !
5. Pratique : journaliser le processus de sérialisation
Ajoutons de la journalisation à notre classe utilisateur afin de voir quand la sérialisation et la désérialisation se produisent.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Sérialisation Person: " + name + ", âge " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Désérialisation Person: " + name + ", âge " + age);
}
}
Utilisation :
Person p = new Person("Bob", 30);
// On enregistre dans un fichier
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// On charge depuis le fichier
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Résultat :
Vous verrez dans la console des messages indiquant que l’objet est sérialisé et désérialisé.
6. Erreurs courantes lors de l’utilisation de writeObject/readObject
Erreur n° 1 : defaultWriteObject/defaultReadObject n’est pas appelé. Si vous oubliez d’appeler ces méthodes, les champs standard ne seront pas sérialisés et l’objet, après désérialisation, sera vide ou incorrect.
Erreur n° 2 : signature des méthodes incorrecte. Les méthodes doivent être strictement private void writeObject(ObjectOutputStream) et private void readObject(ObjectInputStream). Si vous les faites public/protected ou si vous modifiez les paramètres, elles ne seront pas appelées automatiquement.
Erreur n° 3 : exception dans la méthode. Si une exception se produit dans writeObject ou readObject, la sérialisation ou la désérialisation sera interrompue et l’objet ne sera pas correctement enregistré/chargé.
Erreur n° 4 : sérialisation/désérialisation de la superclasse oubliée. Si votre classe hérite d’une autre classe sérialisable, appelez impérativement defaultWriteObject/defaultReadObject, sinon les champs de la superclasse ne seront pas enregistrés.
Erreur n° 5 : sérialisation de données sensibles. Si vous oubliez d’exclure des mots de passe ou d’autres données privées, ils se retrouveront dans le fichier sérialisé. Utilisez transient ou ne les sérialisez pas manuellement.
GO TO FULL VERSION