1. Introduction : sérialisation des objets avec des champs de référence
Dans les applications réelles, on rencontre rarement des classes entièrement « plates ». En général, un objet en contient d’autres, qui peuvent eux-mêmes en contenir encore. On parle de composition (ou d’imbrication) d’objets. Par exemple :
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Objet imbriqué !
}
Quand nous sérialisons un tel objet, une question se pose : que faire du champ address ? Java doit-elle le sérialiser avec Person ? Et si Address contient lui-même un autre objet ? Heureusement (ou malheureusement — selon la situation), Java sérialise par défaut tous les objets imbriqués de manière récursive, s’ils implémentent eux aussi l’interface Serializable.
En Java, la sérialisation est toujours une sérialisation profonde (deep serialization). Cela signifie que non seulement l’objet lui-même est sérialisé, mais aussi tous les objets auxquels il fait référence via ses champs (non transient), et ainsi de suite — jusqu’au « fond ».
Visualisation du processus
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
Bref, si vous décidez de sérialiser Person, Java sérialisera aussi Address, tout ce qu’il contient, et ainsi de suite.
2. Exigences pour les objets imbriqués : Serializable est obligatoire !
Vous avez probablement déjà remarqué un point important : tous les objets imbriqués que l’on sérialise doivent eux aussi implémenter l’interface Serializable.
Si au moins un champ de référence pointe vers un objet qui n’implémente pas Serializable, la tentative de sérialisation se terminera par l’exception java.io.NotSerializableException.
Voyons des exemples de sérialisation profonde.
Exemple : tout va bien
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Les deux classes implémentent Serializable. Tout fonctionne : la sérialisation aboutit avec succès.
Exemple : erreur !
public class Address { // N'implémente PAS Serializable !
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Comme on le voit, ici Address n’implémente pas l’interface Serializable. Donc en sérialisant Person, nous obtiendrons une exception NotSerializableException.
3. Exemple : sérialisation et désérialisation avec un objet imbriqué
Voyons à quoi cela ressemble en pratique. Au niveau précédent, nous avons travaillé avec l’application « Gestionnaire de contacts ». Ajoutons maintenant l’adresse de chaque utilisateur.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("Ivan", 30, new Address("Prague", "Slavinska, 1"));
// Sérialisation
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Désérialisation
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
Résultat :
Ivan, 30, Prague, Slavinska, 1
Tout fonctionne : l’objet imbriqué Address a été sérialisé et restauré avec Person.
4. Que se passe-t-il si l’objet imbriqué n’est pas sérialisable ?
Si vous essayez de sérialiser un objet dont au moins un champ de référence pointe vers un objet qui n’implémente pas Serializable, Java lèvera une exception dès la tentative de sérialisation.
Démonstration de l’erreur
class Address { // pas Serializable !
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "Peter";
p.address = new Address();
p.address.city = "Derry";
p.address.street = "Elm";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- UNE EXCEPTION SERA LANCÉE !
out.close();
}
}
Erreur :
java.io.NotSerializableException: Address
5. Transient pour les objets imbriqués
Que faire si vous avez un champ de référence vers un objet qui ne doit pas être sérialisé (par exemple, un cache, une connexion à la base de données, un objet temporaire) ? Dans ce cas, déclarez le champ comme transient. Java ignorera simplement ce champ lors de la sérialisation.
Exemple avec transient
class Address { // pas Serializable
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient !
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "Los-Santos";
addr.street = "Mulholland Drive";
Person p = new Person("Alex", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Désérialisation
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Alex"
System.out.println(restored.address); // null !
}
}
Résultat :
Le champ address après la désérialisation vaut null, car il est transient.
6. Imbrication profonde et récursion
La sérialisation fonctionne de manière récursive : si Person a un champ Address, et que Address a un champ CityInfo, et ainsi de suite, le sérialiseur va « plonger » de plus en plus profond jusqu’à rencontrer quelque chose de non sérialisable ou jusqu’à ce que la mémoire s’épuise (blague à moitié seulement).
Important : références cycliques
Le sérialiseur Java sait gérer les références cycliques. Si, par exemple, un objet possède une référence vers un autre objet qui, à son tour, pointe en arrière, le sérialiseur ne tournera pas en boucle, mais conservera correctement la structure.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Si vous créez des objets A et B qui se référencent mutuellement, la sérialisation ne provoquera pas de StackOverflowError — Java mémorise les objets déjà sérialisés.
7. Exemple : sérialisation d’un objet avec une liste imbriquée
Il y a souvent, à l’intérieur d’un objet, des collections d’autres objets. Par exemple, un utilisateur peut avoir une liste d’amis :
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// Sérialisation
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Désérialisation
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
Important : Les collections de la bibliothèque standard Java (ArrayList, HashMap, etc.) implémentent Serializable, donc tout fonctionne prêt à l’emploi.
8. Erreurs typiques lors de la sérialisation d’objets imbriqués
Erreur n°1 : l’un des objets imbriqués n’implémente pas Serializable. Vous avez oublié d’ajouter implements Serializable dans l’une des classes imbriquées. Résultat — NotSerializableException dès la première tentative de sérialisation. Vérifiez que toutes les classes de votre chaîne de sérialisation prennent en charge cette interface.
Erreur n°2 : un champ non sérialisable n’est pas déclaré transient. Si vous avez un champ qui ne doit pas être sérialisé (par exemple, un flux, une connexion à une base, quelque chose de temporaire), mais que vous ne l’avez pas déclaré transient, la sérialisation échouera avec une erreur. N’oubliez pas transient !
Erreur n°3 : incompatibilité de serialVersionUID dans des classes imbriquées. Si vous déclarez explicitement serialVersionUID dans des classes imbriquées et que vous modifiez leur structure, n’oubliez pas de mettre à jour cet identifiant — sinon des erreurs de désérialisation sont possibles.
Erreur n°4 : objets imbriqués mutables (modifiables). Si vous sérialisez une collection ou un objet qui est ensuite modifié (par exemple, une liste d’amis), après la désérialisation ce ne sera qu’un « instantané » au moment de la sérialisation. Les nouvelles modifications de l’original ne se refléteront pas dans l’objet désérialisé.
Erreur n°5 : sérialisation d’énormes graphes d’objets. Si votre structure est très complexe, avec de nombreux objets imbriqués, la sérialisation peut prendre beaucoup de temps et de mémoire. Parfois, il vaut mieux sérialiser uniquement les données clés, et non toute la structure.
GO TO FULL VERSION