Astuce Java 76: une alternative à la technique de copie profonde

L'implémentation d'une copie complète d'un objet peut être une expérience d'apprentissage - vous apprenez que vous ne voulez pas le faire! Si l'objet en question fait référence à d'autres objets complexes, qui à leur tour se réfèrent à d'autres, alors cette tâche peut être intimidante. Traditionnellement, chaque classe de l'objet doit être individuellement inspectée et modifiée pour implémenter l' Cloneableinterface et remplacer sa clone()méthode afin de faire une copie complète d'elle-même ainsi que de ses objets contenus. Cet article décrit une technique simple à utiliser à la place de cette longue copie profonde conventionnelle.

Le concept de copie profonde

Afin de comprendre ce qu'est une copie profonde , examinons d'abord le concept de copie superficielle.

Dans un précédent article de JavaWorld , "Comment éviter les interruptions et remplacer correctement les méthodes de java.lang.Object", Mark Roulo explique comment cloner des objets et comment réaliser une copie superficielle au lieu d'une copie profonde. Pour résumer brièvement ici, une copie superficielle se produit lorsqu'un objet est copié sans ses objets contenus. Pour illustrer, la figure 1 montre un objet,, obj1qui contient deux objets, containedObj1et containedObj2.

Si une copie superficielle est effectuée sur obj1, elle est copiée mais ses objets contenus ne le sont pas, comme le montre la figure 2.

Une copie complète se produit lorsqu'un objet est copié avec les objets auxquels il fait référence. La figure 3 montre obj1après qu'une copie complète a été effectuée dessus. Non seulement a obj1été copié, mais les objets qu'il contient ont également été copiés.

Si l'un de ces objets contenus contient eux-mêmes des objets, alors, dans une copie complète, ces objets sont également copiés, et ainsi de suite jusqu'à ce que le graphique entier soit parcouru et copié. Chaque objet est responsable de se cloner via sa clone()méthode. La clone()méthode par défaut , héritée de Object, fait une copie superficielle de l'objet. Pour obtenir une copie complète, une logique supplémentaire doit être ajoutée qui appelle explicitement toutes les clone()méthodes des objets contenus , qui à leur tour appellent les clone()méthodes de leurs objets contenus , etc. Obtenir cela correctement peut être difficile et prendre du temps, et est rarement amusant. Pour rendre les choses encore plus compliquées, si un objet ne peut pas être modifié directement et que sa clone()méthode produit une copie superficielle, alors la classe doit être étendue, leclone()méthode remplacée, et cette nouvelle classe est utilisée à la place de l'ancienne. (Par exemple, Vectorne contient pas la logique nécessaire pour une copie profonde.) Et si vous voulez écrire du code qui reporte à l'exécution la question de savoir s'il faut faire une copie profonde ou superficielle d'un objet, vous êtes dans une encore plus compliquée situation. Dans ce cas, il doit y avoir deux fonctions de copie pour chaque objet: une pour une copie profonde et une pour une copie superficielle. Enfin, même si l'objet copié en profondeur contient plusieurs références à un autre objet, ce dernier objet ne doit toujours être copié qu'une seule fois. Cela évite la prolifération des objets et évite la situation particulière dans laquelle une référence circulaire produit une boucle infinie de copies.

Sérialisation

En janvier 1998, JavaWorld a lancé sa chronique JavaBeans par Mark Johnson avec un article sur la sérialisation, "Faites-le à la manière 'Nescafé' - avec des JavaBeans lyophilisés." Pour résumer, la sérialisation est la capacité de transformer un graphique d'objets (y compris le cas dégénéré d'un seul objet) en un tableau d'octets qui peut être reconverti en un graphique d'objets équivalent. Un objet est dit sérialisable s'il ou l'un de ses ancêtres implémente java.io.Serializableou java.io.Externalizable. Un objet sérialisable peut être sérialisé en le passant à la writeObject()méthode d'un ObjectOutputStreamobjet. Cela écrit les types de données primitifs, les tableaux, les chaînes et les autres références d'objet de l'objet. lewriteObject()est ensuite appelée sur les objets référencés pour les sérialiser également. En outre, chacun de ces objets a ses références et ses objets sérialisés; ce processus se poursuit jusqu'à ce que le graphe entier soit parcouru et sérialisé. Cela vous semble-t-il familier? Cette fonctionnalité peut être utilisée pour obtenir une copie complète.

Copie profonde à l'aide de la sérialisation

Les étapes de création d'une copie complète à l'aide de la sérialisation sont les suivantes:

  1. Assurez-vous que toutes les classes du graphique de l'objet sont sérialisables.

  2. Créez des flux d'entrée et de sortie.

  3. Utilisez les flux d'entrée et de sortie pour créer des flux d'entrée et de sortie d'objet.

  4. Transmettez l'objet que vous souhaitez copier dans le flux de sortie de l'objet.

  5. Lisez le nouvel objet dans le flux d'entrée de l'objet et renvoyez-le dans la classe de l'objet que vous avez envoyé.

J'ai écrit une classe appelée ObjectClonerqui implémente les étapes deux à cinq. La ligne marquée "A" met en place un ByteArrayOutputStreamqui est utilisé pour créer la ObjectOutputStreamligne B. La ligne C est l'endroit où la magie se fait. La writeObject()méthode parcourt le graphe de l'objet de manière récursive, génère un nouvel objet sous forme d'octet et l'envoie au ByteArrayOutputStream. La ligne D garantit que tout l'objet a été envoyé. Le code de la ligne E crée alors un ByteArrayInputStreamet le remplit avec le contenu du ByteArrayOutputStream. La ligne F instancie un en ObjectInputStreamutilisant le ByteArrayInputStreamcréé sur la ligne E et l'objet est désérialisé et renvoyé à la méthode appelante sur la ligne G. Voici le code:

import java.io. *; import java.util. *; import java.awt. *; public class ObjectCloner {// pour que personne ne puisse créer accidentellement un objet ObjectCloner private ObjectCloner () {} // renvoie une copie complète d'un objet static public Object deepCopy (Object oldObj) throws Exception {ObjectOutputStream oos = null; ObjectInputStream ois = null; essayez {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // sérialise et passe l'objet oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = new ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // retourne le nouvel objet return ois.readObject (); // G} catch (Exception e) {System.out.println ("Exception dans ObjectCloner =" + e); lancer (e); } enfin {oos.close (); ois.close (); }}}

Tout ce qu'un développeur ayant accès à ObjectClonerdoit faire avant d'exécuter ce code est de s'assurer que toutes les classes du graphique de l'objet sont sérialisables. Dans la plupart des cas, cela aurait dû être déjà fait; sinon, cela devrait être relativement facile à faire avec l'accès au code source. La plupart des classes du JDK sont sérialisables; seuls ceux qui dépendent de la plate-forme, tels que FileDescriptor, ne le sont pas. De plus, toutes les classes que vous obtenez d'un fournisseur tiers qui sont compatibles JavaBean sont par définition sérialisables. Bien sûr, si vous étendez une classe sérialisable, la nouvelle classe est également sérialisable. Avec toutes ces classes sérialisables flottant, il y a de fortes chances que les seules que vous puissiez avoir besoin de sérialiser soient les vôtres, et c'est un jeu d'enfant par rapport au passage par chaque classe et à l'écrasementclone() pour faire une copie profonde.

Un moyen simple de savoir si vous avez des classes non sérialisables dans le graphique d'un objet est de supposer qu'elles sont toutes sérialisables et d'exécuter ObjectClonerla deepCopy()méthode de celui-ci dessus. S'il y a un objet dont la classe n'est pas sérialisable, alors un java.io.NotSerializableExceptionsera lancé, vous indiquant quelle classe a causé le problème.

Un exemple de mise en œuvre rapide est présenté ci-dessous. Il crée un objet simple v1, qui est un Vectorqui contient un Point. Cet objet est ensuite imprimé pour afficher son contenu. L'objet d'origine,, v1est ensuite copié dans un nouvel objet vNew, qui est imprimé pour montrer qu'il contient la même valeur que v1. Ensuite, le contenu de v1est modifié, et enfin les deux v1et vNewsont imprimés afin que leurs valeurs puissent être comparées.

import java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {try {// récupère la méthode à partir de la ligne de commande String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Utilisation: java Driver1 [deep, shallow]"); revenir; } // créer l'objet original Vector v1 = new Vector (); Point p1 = nouveau point (1,1); v1.addElement (p1); // voir ce que c'est System.out.println ("Original =" + v1); Vecteur vNew = nul; if (meth.equals ("deep")) {// copie profonde vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("shallow")) {// copie superficielle vNew = (Vector) v1.clone (); // B} // vérifier qu'il s'agit du même System.out.println ("New =" + vNew);// change le contenu de l'objet original p1.x = 2; p1.y = 2; // voir ce qu'il y a dans chacun maintenant System.out.println ("Original =" + v1); System.out.println ("Nouveau =" + vNouveau); } catch (Exception e) {System.out.println ("Exception in main =" + e); }}}

Pour appeler la copie complète (ligne A), exécutez java.exe Driver1 deep. Lorsque la copie complète s'exécute, nous obtenons l'impression suivante:

Original = [java.awt.Point [x = 1, y = 1]] Nouveau = [java.awt.Point [x = 1, y = 1]] Original = [java.awt.Point [x = 2, y = 2]] Nouveau = [java.awt.Point [x = 1, y = 1]] 

Cela montre que lorsque l'original Point, p1a été modifié, le nouveau Pointcréé à la suite de la copie profonde n'a pas été affectée, puisque le graphe entier a été copié. Pour comparaison, invoquez la copie superficielle (ligne B) en exécutant java.exe Driver1 shallow. Lorsque la copie superficielle s'exécute, nous obtenons l'impression suivante:

Original = [java.awt.Point [x = 1, y = 1]] Nouveau = [java.awt.Point [x = 1, y = 1]] Original = [java.awt.Point [x = 2, y = 2]] Nouveau = [java.awt.Point [x = 2, y = 2]] 

This shows that when the original Point was changed, the new Point was changed as well. This is due to the fact that the shallow copy makes copies only of the references, and not of the objects to which they refer. This is a very simple example, but I think it illustrates the, um, point.

Implementation issues

Now that I've preached about all of the virtues of deep copy using serialization, let's look at some things to watch out for.

The first problematic case is a class that is not serializable and that cannot be edited. This could happen, for example, if you're using a third-party class that doesn't come with the source code. In this case you can extend it, make the extended class implement Serializable, add any (or all) necessary constructors that just call the associated superconstructor, and use this new class everywhere you did the old one (here is an example of this).

This may seem like a lot of work, but, unless the original class's clone() method implements deep copy, you will be doing something similar in order to override its clone() method anyway.

The next issue is the runtime speed of this technique. As you can imagine, creating a socket, serializing an object, passing it through the socket, and then deserializing it is slow compared to calling methods in existing objects. Here is some source code that measures the time it takes to do both deep copy methods (via serialization and clone()) on some simple classes, and produces benchmarks for different numbers of iterations. The results, shown in milliseconds, are in the table below:

Milliseconds to deep copy a simple class graph n times
Procedure\Iterations(n) 1000 10000 100000
clone 10 101 791
serialization 1832 11346 107725

As you can see, there is a large difference in performance. If the code you are writing is performance-critical, then you may have to bite the bullet and hand-code a deep copy. If you have a complex graph and are given one day to implement a deep copy, and the code will be run as a batch job at one in the morning on Sundays, then this technique gives you another option to consider.

Another issue is dealing with the case of a class whose objects' instances within a virtual machine must be controlled. This is a special case of the Singleton pattern, in which a class has only one object within a VM. As discussed above, when you serialize an object, you create a totally new object that will not be unique. To get around this default behavior you can use the readResolve() method to force the stream to return an appropriate object rather than the one that was serialized. In this particular case, the appropriate object is the same one that was serialized. Here is an example of how to implement the readResolve() method. You can find out more about readResolve() as well as other serialization details at Sun's Web site dedicated to the Java Object Serialization Specification (see Resources).

One last gotcha to watch out for is the case of transient variables. If a variable is marked as transient, then it will not be serialized, and therefore it and its graph will not be copied. Instead, the value of the transient variable in the new object will be the Java language defaults (null, false, and zero). There will be no compiletime or runtime errors, which can result in behavior that is hard to debug. Just being aware of this can save a lot of time.

The deep copy technique can save a programmer many hours of work but can cause the problems described above. As always, be sure to weigh the advantages and disadvantages before deciding which method to use.

Conclusion

L'implémentation d'une copie complète d'un graphe d'objets complexe peut être une tâche difficile. La technique illustrée ci-dessus est une alternative simple à la procédure conventionnelle d'écrasement de la clone()méthode pour chaque objet du graphe.

Dave Miller est un architecte senior au sein du cabinet de conseil Javelin Technology, où il travaille sur les applications Java et Internet. Il a travaillé pour des entreprises telles que Hughes, IBM, Nortel et MCIWorldcom sur des projets orientés objet, et a travaillé exclusivement avec Java au cours des trois dernières années.

En savoir plus sur ce sujet

  • Le site Web Java de Sun comporte une section consacrée à la spécification de sérialisation d'objets Java

    //www.javasoft.com/products/jdk/1.2/docs/guide/serialization/spec/serialTOC.doc.html

Cette histoire, "Java Tip 76: Une alternative à la technique de copie profonde" a été initialement publiée par JavaWorld.