1. NotSerializableException : quand une collection refuse de se sérialiser
L’erreur la plus fréquente et la plus insidieuse lors de la sérialisation des collections — c’est java.io.NotSerializableException. Elle survient si au moins un élément de la collection n’implémente pas l’interface Serializable.
Voyons un exemple naïf :
import java.io.*;
import java.util.*;
class Book {
String title;
Book(String title) { this.title = title; }
}
public class LibraryApp {
public static void main(String[] args) throws Exception {
List<Book> books = new ArrayList<>();
books.add(new Book("Dombey et fils"));
// Tentative de sérialiser la collection
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
oos.writeObject(books); // BOUM ! NotSerializableException
}
}
}
Que va-t-il se passer ? À l’étape oos.writeObject(books), vous obtiendrez l’exception suivante :
java.io.NotSerializableException: Book
Pourquoi ? Parce que la classe Book n’implémente pas l’interface Serializable. Même si la collection elle-même (ArrayList) sait se sérialiser, les éléments de la collection doivent eux aussi être sérialisables !
Comment diagnostiquer
L’erreur indique toujours la classe à l’origine du problème — cherchez-la dans le message d’exception. Si la collection est volumineuse et que l’erreur n’apparaît que dans certaines conditions, il est possible qu’un élément ait été ajouté par inadvertance et n’implémente pas Serializable.
Comment corriger
Ajoutez implements Serializable à votre classe :
class Book implements Serializable {
String title;
Book(String title) { this.title = title; }
}
Conseil : Si la collection contient différents types d’objets, vérifiez qu’ils implémentent tous Serializable !
2. ClassCastException lors de la désérialisation : quand les génériques vous piègent
En Java, l’information sur les paramètres génériques des collections est effacée après compilation (type erasure). Cela signifie que si vous avez sérialisé List<String> et que vous désérialisez en tant que List<Integer>, le compilateur ne verra pas l’erreur, mais à l’exécution vous obtiendrez un ClassCastException.
Exemple :
// Sérialisation
List<String> names = Arrays.asList("Anna", "Boris");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
oos.writeObject(names);
}
// Désérialisation (DANGEREUX !)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
Integer first = numbers.get(0); // BOUM ! ClassCastException
}
Erreur :
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
Comment éviter
- N’utilisez pas de collections « brutes » (raw types) et n’effectuez pas de transtypages sans nécessité.
- Vérifiez les types des éléments après la désérialisation si vous n’êtes pas sûr de leur contenu.
- Documentez le type de collection sérialisé et celui attendu à la lecture.
Exemple de désérialisation sûre :
Object obj = ois.readObject();
if (obj instanceof List<?>) {
List<?> list = (List<?>) obj;
if (!list.isEmpty() && list.get(0) instanceof String) {
@SuppressWarnings("unchecked")
List<String> safeNames = (List<String>) obj; // avertissement supprimé, mais le type a été vérifié !
}
}
3. Modification de la structure des classes : serialVersionUID et rétrocompatibilité
Vous avez sérialisé une collection, puis décidé d’ajouter un nouveau champ à la classe de l’élément, de renommer un champ ou de changer la structure de la classe. Maintenant, lors de la tentative de désérialiser l’ancien fichier, vous obtenez une erreur mystérieuse :
java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678
Pourquoi cela arrive-t-il
Chaque classe sérialisable reçoit un identifiant de version unique — serialVersionUID. Si la classe a changé (par exemple, vous avez ajouté un champ), la JVM calcule un nouveau serialVersionUID, et la désérialisation constate que la version de la classe ne correspond pas à celle utilisée lors de la sérialisation.
Comment éviter
- Déclarez explicitement serialVersionUID dans vos classes :
class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
// ...
}
- Préservez la rétrocompatibilité : n’effacez pas et ne renommez pas les champs si vous prévoyez de lire d’anciens fichiers.
- Testez la désérialisation après les modifications.
Que faire s’il faut malgré tout modifier la classe ?
- Envisagez d’implémenter les méthodes readObject/writeObject pour piloter manuellement la sérialisation.
- Ou migrez les données : lisez l’ancien fichier avec l’ancienne version de la classe, puis réenregistrez au nouveau format.
4. Perte de données lors de la sérialisation des collections immuables
Dans les versions récentes de Java, des collections immuables sont apparues, par exemple créées via List.of(), Set.of(), Map.of(). Dans les anciennes versions de Java (avant 12) et certaines implémentations tierces, la sérialisation de telles collections peut mal fonctionner : après désérialisation, la collection devient ordinaire et modifiable ou une erreur se produit carrément.
Exemple :
List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
oos.writeObject(list);
}
Sur d’anciennes JVM, une erreur survenait à la désérialisation, ou la collection cessait d’être immuable.
Comment éviter
- Vérifiez la documentation de la version de Java que vous utilisez.
- Testez la sérialisation et la désérialisation de ces collections.
- Si vous devez préserver l’immuabilité, après désérialisation, encapsulez la collection avec Collections.unmodifiableList(list).
5. Sérialisation des champs transient et static
Que se passe-t-il pour ces champs :
- transient — les champs marqués par ce mot-clé ne sont pas du tout sérialisés. Après la désérialisation, ils auront la valeur par défaut (par exemple, null ou 0).
- static — les champs de classe (et non d’instance) ne sont jamais sérialisés.
Exemple :
class Book implements Serializable {
String title;
transient String cache; // n’est pas sérialisé !
static String publisher = "Default"; // non plus sérialisé !
}
Pourquoi est-ce important
Si vous stockez des valeurs calculées ou un cache à l’intérieur de l’objet, marquez-les comme transient — cela économise de l’espace et accélère la sérialisation.
Attention : après désérialisation, les champs transient doivent être recalculés ou réinitialisés.
6. Sérialisation de grandes collections : performances et taille de fichier
Problèmes :
- Les grandes collections (p. ex., un million d’objets) peuvent conduire à des fichiers énormes, à des temps d’écriture et de lecture très longs, et parfois même à un manque de mémoire (OutOfMemoryError).
- Lors de la sérialisation d’un graphe d’objets (par exemple, des collections complexes interconnectées), la taille du fichier peut croître de manière inattendue.
Comment éviter
- Sérialisez la collection par morceaux : par exemple, écrivez les objets un par un ou par petites séries.
- Utilisez un traitement en flux : au lieu de sérialiser toute la collection d’un coup, sérialisez les éléments au fil de l’eau.
- Compressez les fichiers : utilisez GZIPOutputStream pour réduire la taille du fichier.
Exemple de sérialisation en flux :
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
for (Book book : bigList) {
oos.writeObject(book);
}
}
Attention : avec cette approche, la désérialisation doit savoir combien d’objets ont été écrits (ou utiliser un « marqueur de fin »).
GO TO FULL VERSION