1. Interface Serializable
Vous vous souvenez des exemples du cours précédent ? Les classes Java que nous souhaitons sérialiser doivent implémenter une interface spéciale — java.io.Serializable. C’est ce qu’on appelle une interface marqueur : elle ne contient aucune méthode et se contente de « marquer » la classe comme apte à la sérialisation. Si la classe implémente cette interface, la JVM autorise la sérialisation de ses objets par les moyens standard.
Il n’est pas souhaitable de tout sérialiser, car tous les objets ne peuvent pas ou ne doivent pas être transformés en octets. Certains objets dépendent de l’état du système d’exploitation, de fichiers ouverts ou de connexions réseau. C’est pourquoi Java exige de marquer explicitement la classe comme sérialisable.
Une interface marqueur, c’est comme un autocollant « autorisé à l’emballage » sur une boîte. Sans cet autocollant, l’emballeur (la JVM) refuse d’opérer.
Exemple : déclaration d’une classe sérialisable
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// Constructeur, getters et setters
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Pour le style : méthode toString()
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
À noter :
- Nous avons simplement ajouté implements Serializable à la déclaration de la classe.
- Inutile d’implémenter des méthodes (l’interface est vide).
- Toutes les classes standard Java sérialisables (par exemple, ArrayList, HashMap, String) implémentent déjà Serializable.
2. Comment rendre sa classe sérialisable
Règle n° 1 : ajoutez simplement implements Serializable
C’est tout ce qu’il faut pour la classe elle-même. Mais il y a des nuances !
Important : tous les objets imbriqués doivent aussi être sérialisables.
Si votre classe contient des champs-références vers d’autres objets, ceux-ci doivent eux aussi être sérialisables. Par exemple :
public class Profile implements Serializable {
private User user; // User doit être sérialisable !
private int level;
}
Si au moins un champ n’est pas sérialisable, une exception sera levée lors de la tentative de sérialisation.
3. Exemple de sérialisation et de désérialisation
Voyons comment sérialiser et désérialiser un objet dans un fichier. Pour cela, on utilise les classes ObjectOutputStream et ObjectInputStream.
Exemple : sérialiser un objet User dans un fichier
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30);
// On enregistre l’objet dans un fichier
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Objet sérialisé avec succès dans le fichier user.ser");
} catch (IOException e) {
System.out.println("Erreur de sérialisation: " + e.getMessage());
}
}
}
Que se passe-t-il ici ?
- Nous créons un objet User.
- Nous ouvrons un flux ObjectOutputStream qui écrit dans le fichier "user.ser".
- Nous appelons writeObject(user). À ce moment-là, la JVM transforme l’objet en flux d’octets et l’enregistre dans le fichier.
Exemple : désérialiser un objet depuis un fichier
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
// On lit l’objet depuis le fichier
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("Objet restauré avec succès: " + user);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Erreur de désérialisation: " + e.getMessage());
}
}
}
Que se passe-t-il ici ?
- Nous ouvrons un flux ObjectInputStream qui lit depuis le fichier "user.ser".
- Nous appelons readObject(). La JVM restaure l’objet à partir des octets.
- N’oubliez pas de convertir le résultat au type voulu (User), car readObject() retourne Object.
- Une ClassNotFoundException peut survenir si la classe User est introuvable lors de la désérialisation.
Le tout ensemble : sérialisation et désérialisation
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Bob", 22);
// Sérialisation
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("Sérialisation terminée!");
} catch (IOException e) {
System.out.println("Erreur de sérialisation: " + e.getMessage());
}
// Désérialisation
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("Désérialisation terminée! " + loaded);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Erreur de désérialisation: " + e.getMessage());
}
}
}
Résultat :
Sérialisation terminée!
Désérialisation terminée! User{name='Bob', age=22}
4. Ce qui se passe « sous le capot » lors de la sérialisation
Lorsque vous appelez writeObject, la JVM vérifie d’abord si la classe implémente l’interface Serializable. Si la classe n’est pas marquée comme sérialisable, une exception est levée. Ensuite, la JVM parcourt tous les champs ordinaires de l’objet (c’est-à-dire ceux qui ne sont pas static et pas transient) et écrit leurs valeurs dans le flux d’octets. Si, parmi ces champs, on rencontre d’autres objets, la sérialisation s’applique de manière récursive, mais uniquement s’ils implémentent eux aussi Serializable.
Lors de la désérialisation, l’objet est créé sans appeler le constructeur habituel, et ses champs sont remplis avec les valeurs sauvegardées — comme un « constructeur sans constructeur » qui redonne vie à l’objet à partir du flux d’octets.
Certains champs ne seront pas sérialisés. Les champs statiques (static) appartiennent à la classe et non à l’instance, leurs valeurs ne sont donc pas sauvegardées. Les champs marqués transient sont également ignorés — pratique pour des données temporaires, un cache ou des informations sensibles comme des mots de passe.
Schéma du processus de sérialisation
flowchart TB
A[Objet User en mémoire] -- writeObject --> B[ObjectOutputStream]
B -- enregistre des octets --> C[Fichier user.ser]
C -- readObject --> D[ObjectInputStream]
D -- restaure --> E[Objet User en mémoire]
5. Erreurs courantes lors de l’utilisation de Serializable
Erreur n° 1 : champ-référence vers un objet non sérialisable. Si la classe User contient un champ de type, par exemple, Thread ou Socket, la sérialisation échouera. Tous les objets ne sont pas sérialisables — gardez-le à l’esprit !
Erreur n° 2 : classes internes non sérialisables. Si la classe User contient une classe interne qui n’est pas static, la sérialisation peut ne pas fonctionner. Il est préférable d’utiliser des classes internes static ou des classes séparées.
Erreur n° 3 : tentative de sérialiser un champ static. Les champs statiques ne sont pas sérialisés — ils appartiennent à la classe, pas à l’objet. Après la désérialisation, le champ static aura la valeur définie dans la classe, et non celle de l’objet sérialisé.
Erreur n° 4 : incompatibilité de versions de la classe. Si vous modifiez la structure de la classe après la sérialisation (par exemple, en ajoutant ou en supprimant un champ), puis tentez de désérialiser un ancien objet, une erreur InvalidClassException peut survenir. Pour le contrôle de version, on utilise le champ spécial serialVersionUID — nous en parlerons plus en détail dans la prochaine leçon.
GO TO FULL VERSION