Java : clonage des objets

Comme on dit, il n’y a que les cons qui ne changent pas d’avis. J’étais jusqu’à récemment un fervent partisan du clonage par Sérialisation, mais je change mon fusil d’épaule.

Partons de l’hypothèse que nous avons une grappe d’objets complexes avec des listes d’objets qui eux mêmes contiennent des listes d’objets etc…

Pour parler d’un cas concret, on utilisera dans la suite les classes suivantes :

  1. class A {
  2.       private List<B> children;
  3.       private C c;
  4.       private D d;
  5. }
  6. class B {
  7.       private A parent;
  8.       private E e;
  9. }
  10. class E {
  11.       private B parent;
  12. }

Clonage par sérialisation

C’est une solution facile à mettre en place et plutôt rapide. Si tous vos objets sont Serialisable, il vous suffit de faire :

  1.     /**
  2.      * Returns a copy of the object, or null if the object cannot
  3.      * be serialized.
  4.      */
  5.     public static Object copy(Object orig) {
  6.         Object obj = null;
  7.         try {
  8.             // Write the object out to a byte array
  9.             ByteArrayOutputStream bos = new ByteArrayOutputStream();
  10.             ObjectOutputStream out = new ObjectOutputStream(bos);
  11.             out.writeObject(orig);
  12.             out.flush();
  13.             out.close();
  14.  
  15.             // Make an input stream from the byte array and read
  16.             // a copy of the object back in.
  17.             ObjectInputStream in = new ObjectInputStream(
  18.                 new ByteArrayInputStream(bos.toByteArray()));
  19.             obj = in.readObject();
  20.         }
  21.         catch(IOException e) {
  22.             e.printStackTrace();
  23.         }
  24.         catch(ClassNotFoundException cnfe) {
  25.             cnfe.printStackTrace();
  26.         }
  27.         return obj;
  28.     }

Et voila, vous êtes capable de cloner votre grappe d’objets sans plus de travail.

Si vous clonez un objet A, vous obtenez un objet A’ réplique parfaite de notre objet initial :

clone par sérialisation

Clone par sérialisation de l'objet parent A

Mais là où il faut faire attention, c’est quand vous voulez cloner un objet qui n’est pas la racine de votre grappe d’objets. Par exemple si on veut cloner un objet E :

Clone par sérialisation

Clone par sérialisation d'un sous objet


On obtient bien un objet E’ qui est le reflet de notre objet initial mais il faut bien comprendre que toute la grappe a été clonée à cause de nos références inverses (de E vers B et B vers A).

Avantages

  • C’est rapide à mettre en place.
  • C’est assez rapide à l’exécution
  • Si les objets évoluent, vous n’avez rien à faire, ça fonctionnera encore
  • Les références cycliques sont gérées
  • Les List et les Map sont gérées

Inconvénients

  • Vous ne maitrisez pas ce qui est cloné
  • Tous les objets sont dupliqués, la consommation mémoire peut devenir énorme si vous faites beaucoup de clonage. La consommation mémoire est multipliée par deux à chaque clone. Si vous avez 4 objets dans votre grappe d’objets, vous en aurez 4 nouveaux en sortie (donc 8 au total).
  • Dans notre exemple, le clonage d’un objet B clone aussi son parent A et avec lui tous les autres objets B enfants de A. Ce qui dans certains cas n’est pas le comportement voulu.

Clonage manuel

Vous définissez vous même toutes les méthodes clone() dans vos objets et mettez de la logique dans vos clones : est-ce que ce champs doit être cloné ? et celui-là ? et cet autre là ?

Par exemple, si les classes C et D ne peuvent pas être modifiées, le clone d’un objet A donnerait un objet A’ qui conserverait les références vers les objets C et D.

Clonage manuel

Clonage manuel de l'objet racine

La logique de clone de l’objet E peut alors être différente et partir du principe qu’il ne clone pas son parent B, ce qui donnerait :

Clonage manuel

Clonage manuel d'un sous objet

Nous arrivons donc à cloner uniquement les objets qui ont vraiment du sens dans notre projet.

Avantages

  • Vous maitrisez exactement ce qui est fait
  • Vous pouvez cloner seulement ce qui est modifiable par l’utilisateur et garder les mêmes références pour ce qui n’est pas modifiable. Exemple : dans notre classe A qui possède une liste d’objets B. Le clone de notre classe B peut imposer de garder la référence vers l’objet A parent. Ou peut-être en fonction de vos besoins la remise à null du parent.

Inconvénients

  • Vous devez vous même traiter le cas des listes (en gérant les null).
  • Vous devez gérer le clone des objets complexes
  • Vos clones sont à refaire/maintenir si vos objets évolues (ajout ou suppression de champs)

Raison de mon changement d’avis

Contexte applicatif

Je travail sur un projet où nous affichons un arbre à l’utilisateur (TreeView) chaque noeud de cet arbre est lié à de nombreux objets issus de la base de données. Par exemple, le statut de chaque ligne est un objet, le type de la ligne est un objet.
Il va sans dire que nous avions mis en place le clone par sérialisation, au début tout allait bien dans le meilleur des mondes.

Et nous avons livré une fonctionnalité très attendue : le bouton « dupliquer » qui duplique un noeud de l’arbre et ses fils.
Ce bouton fait simplement appel au clone du noeud sélectionné et l’ajoute dans le noeud parent.

Et là, les utilisateurs ont commencé à nous remonter des lenteurs extrêmes sur l’application. On ne pouvait pas dupliquer plus d’une centaine de lignes sans que l’application crache des OutOfMemory Java Heap Space à la tête de l’utilisateur.

Investigations et corrections

Après investigation, le clone de chaque noeud aboutissait au clone de l’ensemble de la grappe d’objet et pas seulement du noeud sélectionné ! (à cause des liens bidirectionnels entre toutes les classes).

Les lenteurs étaient dues à la sur-consommation mémoire qui oblige le garbage collector à tourner à fond pour tenter de libérer la mémoire.

Ma première correction a été de remettre à null le parent pour libérer la mémoire, mais ce n’est vraiment pas propre. Car on clone toute la grappe d’objet pour en garder qu’une très petite portion.

Donc j’ai continué. Je suis retourné au clonage manuel :

Seuls certains objets étaient modifiables par l’utilisateur (j’entends le contenu de ces objets et pas simplement je remplace une référence d’objet par une autre référence à un autre objet). Par exemple le type de mes lignes provient de la base de données, je peux changer le type en remplaçant l’objet par un autre provenant de la bdd, mais je ne modifie pas le contenu de l’objet lui même.

Donc j’ai cloné uniquement les objets modifiables et j’ai conservé les références aux objets qui ne peuvent être que « remplacés » par une autre référence.

Résultat, la fonctionnalité de duplication de mes noeuds est devenue très rapide, et surtout peu consommatrice en mémoire…

Conclusion : N’utilisez pas le clone par Sérialisation

Tout pousse à utiliser le clonage par sérialisation, c’est facile à mettre en place, ça ne demande pas de maintenance même quand les objets évoluent mais… dans certains cas ça devient vraiment catastrophique !

Donc je vous déconseille vraiment d’utiliser le clone par sérialisation.

Si contre toutes mes recommandations, vous utilisez le clone par sérialisation, ça peut vous couter cher par la suite quand il faudra repassez dans tous vos objets et réécrire les clones à la main.

Le commentaires sont fermés.