1. Sérialisation des collections à l’intérieur de collections
En Java, les collections peuvent contenir non seulement des types simples (par exemple, String), mais aussi d’autres collections ou objets. Cela ouvre la voie à la création de structures complexes : par exemple, une Map<String, List<User>>, où User est votre propre classe.
Exemple : sérialisation d’une Map avec une List imbriquée
Prenons l’exemple d’un petit réseau social où chaque utilisateur a une liste d’amis.
import java.io.*;
import java.util.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + '}';
}
}
public class SocialNetwork implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<User>> friends = new HashMap<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
SocialNetwork network = new SocialNetwork();
network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
network.friends.put("bob", Collections.singletonList(new User("alice")));
// Sérialisation
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
out.writeObject(network);
}
// Désérialisation
SocialNetwork loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
loaded = (SocialNetwork) in.readObject();
}
System.out.println("Réseau restauré : " + loaded.friends);
}
}
On peut le dire ainsi : tous les types utilisés — qu’il s’agisse de HashMap, ArrayList ou User — implémentent l’interface Serializable. Lors de la sérialisation, Java parcourt automatiquement toutes les collections et objets imbriqués, et les écrit eux aussi. Par conséquent, après la désérialisation, vous obtenez une structure entièrement restaurée, y compris toutes les listes imbriquées.
Sortie :
Réseau restauré : {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}
Imbrication à volonté
Vous pouvez créer autant de niveaux d’imbrication que vous le souhaitez : List<List<User>>, Map<String, Map<Integer, List<User>>> — Java n’a pas peur de la récursion (dans des limites raisonnables, bien sûr).
2. Objets hiérarchiques : sérialisation de collections avec héritage
Et si vos collections contiennent des objets construits selon le principe de l’héritage ? Par exemple, vous avez une classe de base Animal, et la collection contient à la fois des Cat et des Dog ?
Exemple : sérialisation d’une collection avec des héritiers
import java.io.*;
import java.util.*;
abstract class Animal implements Serializable {
private static final long serialVersionUID = 1L;
String name;
Animal(String name) {
this.name = name;
}
public abstract String speak();
}
class Cat extends Animal {
private static final long serialVersionUID = 1L;
Cat(String name) {
super(name);
}
@Override
public String speak() {
return "Meow!";
}
}
class Dog extends Animal {
private static final long serialVersionUID = 1L;
Dog(String name) {
super(name);
}
@Override
public String speak() {
return "Woof!";
}
}
public class Zoo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat("Mittens"));
animals.add(new Dog("Buddy"));
// Sérialisation
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
out.writeObject(animals);
}
// Désérialisation
List<Animal> loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
loaded = (List<Animal>) in.readObject();
}
for (Animal animal : loaded) {
System.out.println(animal.name + " dit : " + animal.speak());
}
}
}
Résultat :
Mittens dit : Meow!
Buddy dit : Woof!
Point important : Java sérialise non seulement les champs de la classe de base, mais aussi l’information sur le type réel de l’objet. Ainsi, après la désérialisation, les objets conservent leur nature « féline » ou « canine », et vous pouvez appeler leurs méthodes en toute sécurité.
3. Sérialisation de graphes d’objets
Il est temps de passer à la véritable magie — la sérialisation de graphes d’objets, où les objets peuvent se référer les uns aux autres, et pas seulement être imbriqués. Mais commençons par clarifier ce qu’est un graphe d’objets.
Qu’est-ce qu’un graphe d’objets ?
Un graphe d’objets est une structure où les objets peuvent être liés entre eux via des champs de référence. Par exemple, dans un arbre généalogique, chaque personne peut avoir des références vers ses parents, ses enfants, ses frères et sœurs.
Analogie : Imaginez un groupe d’amis sur un réseau social : chaque utilisateur a une liste d’amis, et ces amis sont eux-mêmes des utilisateurs qui ont leurs propres amis, et ainsi de suite. C’est un graphe d’objets.
Exemple : sérialisation d’une liste doublement chaînée
import java.io.*;
class Node implements Serializable {
private static final long serialVersionUID = 1L;
String value;
Node next;
Node prev;
Node(String value) {
this.value = value;
}
}
public class DoublyLinkedListDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Créons deux nœuds liés
Node first = new Node("A");
Node second = new Node("B");
first.next = second;
second.prev = first;
// Sérialisation
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
out.writeObject(first);
}
// Désérialisation
Node loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
loaded = (Node) in.readObject();
}
System.out.println("Valeur du premier : " + loaded.value); // "A"
System.out.println("Suivant : " + loaded.next.value); // "B"
System.out.println("Précédent du suivant : " + loaded.next.prev.value); // "A"
}
}
Remarquez qu’une seule référence (first) est sérialisée, mais grâce à la sérialisation récursive, Java va « parcourir » tous les objets liés. À la désérialisation, la structure des références est entièrement restaurée : loaded.next.prev == loaded sera true ! Et si le graphe contient des cycles (par exemple lorsque des nœuds se référencent mutuellement), la sérialisation standard de Java fonctionne correctement et ne boucle pas indéfiniment.
4. Collections imbriquées et hiérarchiques : exemple avec une classe réelle
Modèle : catalogue de livres
Supposons que nous ayons une classe Book, qui peut être un livre papier ou une édition électronique (héritage). Nous avons aussi une classe Library, qui contient une carte des genres (Map<String, List<Book>>). Chaque genre est une liste de livres.
import java.io.*;
import java.util.*;
abstract class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
Book(String title) {
this.title = title;
}
}
class PaperBook extends Book {
private static final long serialVersionUID = 1L;
int pages;
PaperBook(String title, int pages) {
super(title);
this.pages = pages;
}
}
class EBook extends Book {
private static final long serialVersionUID = 1L;
String format;
EBook(String title, String format) {
super(title);
this.format = format;
}
}
class Library implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<Book>> catalog = new HashMap<>();
}
public class CatalogDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Library library = new Library();
library.catalog.put("Science-fiction", Arrays.asList(
new PaperBook("Dune", 800),
new EBook("Seul sur Mars", "epub")
));
library.catalog.put("Classiques", Collections.singletonList(
new PaperBook("Guerre et paix", 1200)
));
// Sérialisation
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
out.writeObject(library);
}
// Désérialisation
Library loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
loaded = (Library) in.readObject();
}
for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
System.out.println("Genre : " + entry.getKey());
for (Book book : entry.getValue()) {
System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
}
}
}
}
Sortie :
Genre : Science-fiction
- Dune (PaperBook)
- Seul sur Mars (EBook)
Genre : Classiques
- Guerre et paix (PaperBook)
Bilan :
- La sérialisation des collections imbriquées (Map<String, List<Book>>) fonctionne « prête à l’emploi ».
- Les types d’objets (PaperBook, EBook) sont conservés.
- Après la désérialisation, la structure est entièrement restaurée.
5. Sérialisation de graphes d’objets : que se passe-t-il « sous le capot » ?
Lorsque vous sérialisez un objet, Java « parcourt » tous ses champs (et les champs des champs, etc.), en sérialisant chaque objet une seule fois. Si un objet est rencontré de nouveau (par exemple dans une référence cyclique), Java écrit une référence spéciale au lieu de le sérialiser une seconde fois.
Visualisation (organigramme)
graph TD
A[Objet A] -- champ --> B[Objet B]
B -- champ --> C[Objet C]
C -- champ --> A
Java sérialise d’abord A, puis B, puis C ; et lorsqu’elle rencontre de nouveau A, elle écrit « une référence vers l’objet A déjà sérialisé ». À la désérialisation, la structure est reconstruite avec toutes les liaisons conservées.
6. Particularités de la sérialisation de graphes
- Les cycles ne posent pas de problème : la sérialisation standard de Java prend en charge les références cycliques, ne boucle pas à l’infini et n’entraîne pas de StackOverflow.
- Tous les objets doivent être sérialisables : si au moins un objet dans le graphe n’implémente pas Serializable, la sérialisation échouera sur cet objet.
- Les mêmes objets ne sont pas dupliqués : si le même objet apparaît à plusieurs endroits du graphe, après la désérialisation ce sera le même objet (par référence).
- Les types d’objets sont conservés : même si la collection est déclarée comme List<Animal>, après la désérialisation vous obtiendrez des objets de leurs classes réelles (Cat, Dog, etc.).
7. Erreurs fréquentes lors de la sérialisation d’objets imbriqués et hiérarchiques
Erreur n° 1 : Tous les classes ne sont pas sérialisables.
On oublie très souvent d’ajouter implements Serializable à l’une des classes maison qui se trouve à l’intérieur d’une collection ou d’un objet imbriqué. Résultat : NotSerializableException et déception. Vérifiez toute la chaîne d’imbrication !
Erreur n° 2 : Perte de références lors d’une sérialisation manuelle.
Si vous implémentez vous-même les méthodes writeObject/readObject et oubliez de sérialiser l’un des champs (par exemple, une référence vers le parent ou vers une collection imbriquée), après la désérialisation la structure sera corrompue. Testez toujours la restauration.
Erreur n° 3 : Utilisation de transient pour des champs nécessaires.
Si vous marquez un champ nécessaire comme transient, il ne sera pas écrit dans le flux sérialisé et, après restauration, vaudra null ou sa valeur par défaut. Cela peut rompre l’intégrité du graphe d’objets.
Erreur n° 4 : Changement de structure des classes entre sérialisation et désérialisation.
Si vous avez modifié la structure d’une classe (par exemple, ajouté un champ) après avoir sérialisé un objet, vous risquez des erreurs ou une perte de données lors de la désérialisation. Utilisez serialVersionUID et maintenez la compatibilité.
Erreur n° 5 : Sérialisation de grands graphes.
Des structures très interconnectées peuvent conduire à des fichiers très volumineux et à une sérialisation/désérialisation longue. Surveillez les tailles et, si possible, découpez en parties.
Erreur n° 6 : Sérialisation de collections « brutes ».
Si vous déclarez une collection sans paramètre générique (par exemple simplement List), après la désérialisation vous devrez effectuer des conversions explicites de types, ce qui peut mener à une ClassCastException. Utilisez des génériques et vérifiez les types.
GO TO FULL VERSION