CodeGym /Cours /JAVA 25 SELF /Sérialisation des collections génériques : particula...

Sérialisation des collections génériques : particularités

JAVA 25 SELF
Niveau 45 , Leçon 1
Disponible

1. Sérialisation et désérialisation des collections génériques

Les génériques (généralisation) en Java ne sont pas de la magie, mais plutôt une illusion maintenue par le compilateur. À l’étape de compilation, l’information sur les types paramètres des génériques est effacée (cela s’appelle type erasure, ou « effacement des types »). Autrement dit, à l’exécution (runtime), une collection List<String> ne se distingue en rien d’une List<Object> ou d’une List<Integer>. Ce sont toutes des List, et la JVM ne sait pas quels types exacts s’y trouvent.

Considérons un exemple :

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

Ici, tout est subtil. Nous créons deux listes — l’une pour les chaînes, l’autre pour les nombres. Au niveau du compilateur, Java veille strictement à ce que vous ne mettiez rien d’autre que des chaînes dans List<String>, et rien d’autre que des nombres dans List<Integer>. Mais dès que le programme démarre, les différences disparaissent. Pour la JVM, les deux objets ne sont que des ArrayList, et elle ne peut plus vérifier quels éléments doivent s’y trouver. C’est précisément pourquoi la comparaison des classes des deux listes (stringList.getClass() == intList.getClass()) renvoie true.

D’où une conclusion importante : les génériques en Java servent avant tout au confort et à la sécurité au moment de la compilation. Mais à l’exécution, ces « étiquettes » se perdent. Ainsi, si vous sérialisez une collection, seuls les données elles-mêmes iront dans le fichier, et non les informations sur les types génériques. Autrement dit, la liste des valeurs sera conservée, mais on ne peut pas savoir, à partir du fichier, si c’était précisément une List<String>, une List<Object> ou une List<Integer>.

Encore un exemple : sérialisation et désérialisation de List<String>

import java.io.*;
import java.util.*;

public class GenericSerializationDemo {
    public static void main(String[] args) throws Exception {
        List<String> fruits = new ArrayList<>();
        fruits.add("Pomme");
        fruits.add("Banane");
        fruits.add("Orange");

        // Sérialisation
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
            oos.writeObject(fruits);
        }

        // Désérialisation
        List<String> loadedFruits;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
            loadedFruits = (List<String>) ois.readObject();
        }

        System.out.println(loadedFruits); // [Pomme, Banane, Orange]
    }
}

Remarquez la ligne suivante :

loadedFruits = (List<String>) ois.readObject();

Ici, nous convertissons explicitement le résultat en type List<String>, alors qu’en réalité, à l’exécution, ce n’est qu’une ArrayList. Le compilateur ne pourra pas vérifier qu’il s’agit bien d’une liste de chaînes, et si jamais on y trouve autre chose que des chaînes — on obtiendra une ClassCastException au moment de l’exécution.

2. Problèmes lors de la désérialisation des collections génériques

Perte d’information sur le type des éléments

Puisque l’information sur les paramètres génériques est effacée, après la désérialisation, Java ne peut pas garantir que la collection contient précisément les objets attendus. Tout ce que vous obtenez — c’est une collection « brute » (raw type), et le compilateur ne se plaint pas, mais le problème peut survenir à l’exécution.

Démonstration du problème

List rawList = new ArrayList();
rawList.add("Chat");
rawList.add(42); // Integer!
// Désérialisation
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!

Avertissement « Unchecked cast »

Le compilateur vous avertira honnêtement d’un problème potentiel :

Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Cet avertissement indique que vous effectuez une conversion de type sans vérification, et que la collection peut contenir des objets d’un type inattendu.

3. Particularités de la sérialisation des collections génériques

Aucune information sur les paramètres génériques dans le fichier

Lorsque vous sérialisez une List<String> et une List<Integer>, le fichier ne contient aucune information indiquant qu’il s’agissait de chaînes ou de nombres. Le contenu de la collection est sérialisé « en l’état » — des objets, dans l’ordre.

Si vous ouvrez le fichier sérialisé dans un éditeur de texte, vous n’y verrez aucun mot à propos de <String> ou de <Integer>. Tout cela n’existe qu’au niveau du code source et du compilateur.

Exemple : sérialisation de collections différentes

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("un", "deux", "trois");

// Sérialisation
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
    oos.writeObject(numbers);
    oos.writeObject(words);
}

Dans le fichier test.ser, il y a simplement deux objets de type ArrayList, sans aucune information sur les paramètres génériques.

Problème de désérialisation des collections « brutes »

Si vous avez sérialisé une liste sans paramètre générique (raw type) et la désérialisez comme List<String>, le compilateur ne pourra pas vérifier la correction des types, et des erreurs à l’exécution sont possibles.

4. Bonnes pratiques pour la sérialisation des collections génériques

Documentez les types d’éléments attendus.
Si votre API sérialise une collection, indiquez impérativement quel type d’éléments est attendu. Par exemple : « Cette méthode renvoie une List<User> sérialisée ».

Vérifiez les types des éléments après la désérialisation.
Après la désérialisation d’une collection, il est utile de vérifier que tous les éléments ont le type attendu (surtout si la source des données n’est pas sous votre contrôle).

for (Object obj : loadedList) {
    if (!(obj instanceof String)) {
        throw new IllegalStateException("Chaîne attendue, mais trouvé: " + obj.getClass());
    }
}

Utilisez des collections immuables.
Si vous sérialisez une collection en lecture seule, utilisez des collections immuables — List.copyOf, Collections.unmodifiableList. Cela aide à éviter les modifications accidentelles des données après désérialisation.

Ne mélangez pas les types dans une même collection.
Évitez de sérialiser des collections contenant des éléments de types différents (par exemple, une List<Object> avec différentes classes à l’intérieur). Cela complique la désérialisation et peut mener à des erreurs.

Utilisez la suppression des avertissements avec prudence.
Si vous êtes certain de désérialiser une collection avec le bon type d’éléments, vous pouvez supprimer l’avertissement du compilateur à l’aide de l’annotation @SuppressWarnings("unchecked") :

@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();

Mais utilisez cela en connaissance de cause — il est facile de masquer un problème jusqu’à la production.

5. Exemple : sérialisation et désérialisation d’une collection avec une classe personnalisée

Supposons que nous ayons une classe User :

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

Sérialisons une liste d’utilisateurs :

List<User> users = Arrays.asList(
    new User("Alice", 30),
    new User("Bob", 25)
);

// Sérialisation
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
    oos.writeObject(users);
}

// Désérialisation
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
    loadedUsers = (List<User>) ois.readObject();
}

System.out.println(loadedUsers); // [Alice (30), Bob (25)]

Tout fonctionne ! Mais si quelqu’un glisse dans le fichier sérialisé un objet d’une autre classe, vous pouvez obtenir une ClassCastException en tentant de lire les éléments comme des User.

6. Sérialisation de collections génériques imbriquées

Les collections peuvent être imbriquées, par exemple : List<List<String>>, Map<String, List<User>>, etc. Java sérialise ces structures récursivement, mais les règles restent les mêmes :

  • Toutes les collections imbriquées et tous les éléments doivent être sérialisables.
  • L’information sur les paramètres génériques est toujours effacée.

Exemple : sérialisation d’une liste de listes

List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));

// Sérialisation
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
    oos.writeObject(matrix);
}

// Désérialisation
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
    loadedMatrix = (List<List<String>>) ois.readObject();
}

System.out.println(loadedMatrix); // [[a, b], [c, d]]

7. Subtilités utiles

Sérialisation de collections génériques avec des implémentations différentes

Il arrive que vous sérialisiez une implémentation de collection et désérialisiez comme une autre. Par exemple, vous avez sérialisé une ArrayList, puis désérialisez comme une LinkedList. Cela conduira à une erreur de conversion de type :

List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!

Conseil : désérialisez toujours dans le même type que celui qui a été sérialisé, ou utilisez l’interface (List) si l’implémentation concrète n’a pas d’importance pour vous.

Utilisation de bibliothèques (par ex., Gson, Jackson)

Les bibliothèques pour JSON (par exemple, Gson, Jackson) savent sérialiser/désérialiser des collections génériques, mais nécessitent une indication explicite du type à la désérialisation en raison de l’effacement des types. Exemple pour Gson :

Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

8. Génériques et sérialisation dans Map et Set

Toutes les règles ci‑dessus s’appliquent également aux autres collections génériques :

  • Lors de la sérialisation d’une Map<String, Integer>, l’information sur les types des clés et des valeurs n’est pas conservée.
  • À la désérialisation, il faut convertir vers le type voulu et être attentif au contenu.

Exemple : sérialisation d’une Map

Map<String, Integer> scores = new HashMap<>();
scores.put("Vasya", 90);
scores.put("Petya", 85);

// Sérialisation
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
    oos.writeObject(scores);
}

// Désérialisation
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
    loadedScores = (Map<String, Integer>) ois.readObject();
}

System.out.println(loadedScores); // {Vasya=90, Petya=85}

9. Erreurs courantes lors de la sérialisation des collections génériques

Erreur n° 1 : ClassCastException à la désérialisation. Si vous désérialisez une collection comme List<String>, mais qu’elle contient un objet d’un autre type, vous obtiendrez une ClassCastException à l’exécution. Vérifiez toujours le contenu de la collection !

Erreur n° 2 : NotSerializableException à cause d’un élément non sérialisable. Si au moins un élément de la collection n’implémente pas Serializable, la sérialisation échouera avec une NotSerializableException. Vérifiez la sérialisabilité de toutes les classes susceptibles de se trouver dans la collection.

Erreur n° 3 : Perte d’information sur les paramètres génériques. Après la désérialisation, ne comptez pas sur les paramètres génériques — ils n’existent pas à l’exécution. Utilisez des vérifications explicites des types si vous doutez de la validité des données.

Erreur n° 4 : Décalage entre les implémentations de collections. Vous avez sérialisé une ArrayList, mais désérialisez comme LinkedList — vous obtiendrez une erreur de conversion. Essayez de désérialiser dans le même type que celui sérialisé.

Erreur n° 5 : Incompatibilité des versions de classes. Si la structure de la classe d’un élément de la collection a changé après la sérialisation (par exemple, ajout d’un champ), des erreurs à la désérialisation sont possibles. Utilisez serialVersionUID pour la gestion des versions.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION