CodeGym /Cours /JAVA 25 SELF /Problème des références cycliques : détection, contournem...

Problème des références cycliques : détection, contournement

JAVA 25 SELF
Niveau 44 , Leçon 2
Disponible

1. Qu’est-ce qu’une référence cyclique ?

Une référence cyclique est une situation où un objet (ou une collection) contient, directement ou indirectement, une référence vers lui-même. Dans les collections, cela arrive plus souvent qu’on ne le croit, surtout si vous construisez des structures de données complexes ou travaillez avec des graphes.

Exemples concrets

  • Deux objets se référencent mutuellement :
    Par exemple, vous avez une classe User qui a une référence vers Profile, et Profile a une référence inverse vers User.
  • Une collection se contient elle-même :
    L’exemple le plus simple et « amusant » :
List<Object> list = new ArrayList<>();
list.add(list); // Oups ! la liste se contient elle-même
  • Un graphe d’objets :
    Des objets interconnectés, par exemple des nœuds d’un arbre, où chacun peut avoir une référence vers son parent et ses enfants.

Visualisation

graph LR
A[User] -- profile --> B[Profile]
B -- user --> A

Ou pour une collection :

graph TD
L[List] -- add(self) --> L

Pourquoi cela peut-il poser problème ?

Si le sérialiseur ne sait pas gérer les cycles, il peut entrer dans une récursion infinie en essayant de sérialiser encore et encore les objets imbriqués, jusqu’à un dépassement de pile (StackOverflowError). Bonne nouvelle : la sérialisation standard de Java connaît ces cas et sait les contourner !

2. Comment la sérialisation standard de Java gère-t-elle les cycles ?

Lorsque vous sérialisez un objet via ObjectOutputStream, Java suit automatiquement quels objets ont déjà été sérialisés dans ce flux. Si le sérialiseur rencontre un objet une seconde fois, il ne le sérialise pas de nouveau, mais écrit une référence spéciale vers l’objet déjà sérialisé. Cela permet de sérialiser correctement même des structures très complexes contenant des cycles.

Exemple : une collection qui se contient elle-même

Essayons de sérialiser une collection qui se contient elle-même. Ce n’est pas une blague — ce code se compile et fonctionne même :

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

public class CyclicListDemo {
    public static void main(String[] args) throws Exception {
        List<Object> list = new ArrayList<>();
        list.add("Hello, cyclic world!");
        list.add(list); // On s’ajoute soi-même

        // Sérialisation
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic_list.ser"))) {
            out.writeObject(list);
        }

        // Désérialisation
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic_list.ser"))) {
            List<?> deserialized = (List<?>) in.readObject();

            System.out.println(deserialized.get(0)); // "Hello, cyclic world!"
            System.out.println(deserialized.get(1) == deserialized); // true!
        }
    }
}

Résultat :
— Le premier élément est une chaîne ordinaire.
— Le deuxième élément est... la collection elle-même ! Le test deserialized.get(1) == deserialized retournera true.
Java ne s’est pas retrouvée dans une boucle et n’a pas planté : la structure des références a été correctement restaurée.

Comment cela fonctionne-t-il en interne ?

ObjectOutputStream maintient un « registre » interne des objets sérialisés. Si un objet a déjà été sérialisé, une référence spéciale (handle) vers celui-ci est écrite dans le flux, au lieu de son contenu. À la désérialisation, ObjectInputStream restaure ces mêmes liens.

3. Problèmes et limitations

  • Vous avez accidentellement sérialisé un graphe énorme.
    Si votre structure de données est très grande et contient de nombreuses références croisées, la sérialisation peut prendre beaucoup de temps et produire un fichier immense.
  • Modification de la structure des classes.
    Si vous avez sérialisé un objet puis modifié sa classe (par exemple, en ajoutant ou supprimant un champ), la désérialisation peut provoquer une InvalidClassException. C’est particulièrement vrai si les champs modifiés participent à un cycle.
  • Problèmes avec une sérialisation personnalisée.
    Si vous implémentez manuellement writeObject et readObject, vous devez gérer correctement les cycles vous-même. Si vous oubliez d’appeler les méthodes par défaut (defaultWriteObject/defaultReadObject), le sérialiseur ne pourra pas suivre les cycles.
  • Sérialisation vers d’autres formats (par exemple, JSON).
    La sérialisation standard de Java (ObjectOutputStream) gère les cycles, mais si vous sérialisez des objets en JSON (par exemple via Jackson ou Gson), les cycles peuvent entraîner un StackOverflowError ou des exceptions. Ces bibliothèques ne gèrent pas les cycles par défaut — une configuration explicite est nécessaire.

4. Contournement des références cycliques

Dans la sérialisation standard de Java

Tout fonctionne « prêt à l’emploi » ! Vous n’avez rien de spécial à faire — Java détectera elle-même les cycles et préservera la structure des références.

Manuellement : sérialisation vers d’autres formats

  • Utiliser des identifiants à la place des références.
    Au lieu de stocker des références vers d’autres objets, stockez leurs identifiants uniques. Après désérialisation, restaurez les liens à partir de ces id.
  • Annotations ou réglages spécifiques.
    Dans Jackson, vous pouvez utiliser les annotations @JsonIdentityInfo ou le duo @JsonBackReference/@JsonManagedReference pour contrôler la sérialisation des cycles.
  • Supprimer les cycles avant la sérialisation.
    Mettez temporairement à null les champs qui créent un cycle, ou excluez-les à l’aide de transient ou d’annotations.

Exemple : sérialiser un graphe avec des cycles

Considérons un exemple avec une structure plus complexe — un graphe d’utilisateurs, où chaque utilisateur peut être l’ami d’un autre.

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

class User implements Serializable {
    String name;
    List<User> friends = new ArrayList<>();

    User(String name) { this.name = name; }

    public String toString() {
        return name + " (" + friends.size() + " friends)";
    }
}

public class CyclicGraphDemo {
    public static void main(String[] args) throws Exception {
        User alice = new User("Alice");
        User bob = new User("Bob");
        User charlie = new User("Charlie");

        // Créons des relations d’amitié cycliques
        alice.friends.add(bob);
        bob.friends.add(charlie);
        charlie.friends.add(alice); // cycle !

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

        // Désérialisation
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("users.ser"))) {
            User restoredAlice = (User) in.readObject();
            System.out.println(restoredAlice);
            System.out.println(restoredAlice.friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0));
            System.out.println(restoredAlice.friends.get(0).friends.get(0).friends.get(0) == restoredAlice); // true!
        }
    }
}

Résultat :
— La structure cyclique est restaurée : après trois sauts via les amis, on retombe sur Alice.
— Java ne s’est ni embrouillée ni mise à boucler.

5. Erreurs courantes avec les références cycliques

Erreur n° 1 : sérialiser en JSON sans gestion des cycles. Si vous choisissez de sérialiser un objet comportant des cycles via Jackson ou Gson sans configuration, vous obtiendrez très probablement un StackOverflowError. Par exemple, si vous avez une classe Node où chaque nœud référence son parent et ses enfants, la sérialisation d’un tel arbre en JSON conduira à une imbrication infinie.

Erreur n° 2 : modification de la structure des classes. Si, après la sérialisation, vous modifiez la structure d’une classe (par exemple en ajoutant un champ), la désérialisation d’un ancien fichier peut provoquer une erreur d’incompatibilité. C’est particulièrement critique pour des graphes complexes avec cycles.

Erreur n° 3 : sérialisation maison sans prise en compte des cycles. Si vous implémentez writeObject/readObject manuellement et que vous n’appelez pas defaultWriteObject, Java ne pourra pas suivre les cycles, et la sérialisation soit bouclera, soit la structure des références sera cassée à la désérialisation.

Erreur n° 4 : ajouter la collection à elle-même par inadvertance. Il arrive que des développeurs débutants ajoutent accidentellement une collection à elle-même (par exemple, lors d’une copie d’éléments) sans se rendre compte qu’ils ont créé un cycle. La sérialisation fonctionnera, mais la logique de l’application pourra devenir étrange et imprévisible.

1
Mission
JAVA 25 SELF, niveau 44, leçon 2
Bloqué
Liste paradoxale : une collection qui se contient elle-même 🤯
Liste paradoxale : une collection qui se contient elle-même 🤯
1
Mission
JAVA 25 SELF, niveau 44, leçon 2
Bloqué
Sauvegarde de la topologie du réseau : sérialisation d'un graphe avec un cycle 🌐
Sauvegarde de la topologie du réseau : sérialisation d'un graphe avec un cycle 🌐
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION