Suggerimento Java 76: un'alternativa alla tecnica della copia profonda

L'implementazione di una copia profonda di un oggetto può essere un'esperienza di apprendimento: impari che non vuoi farlo! Se l'oggetto in questione si riferisce ad altri oggetti complessi, che a loro volta si riferiscono ad altri, allora questo compito può essere davvero scoraggiante. Tradizionalmente, ogni classe nell'oggetto deve essere ispezionata e modificata individualmente per implementare l' Cloneableinterfaccia e sovrascrivere il suo clone()metodo al fine di creare una copia completa di se stessa e degli oggetti contenuti. Questo articolo descrive una tecnica semplice da utilizzare al posto di questa copia profonda convenzionale che richiede tempo.

Il concetto di copia profonda

Per capire cos'è una copia profonda , diamo prima un'occhiata al concetto di copia superficiale.

In un precedente articolo di JavaWorld , "Come evitare le trappole e sovrascrivere correttamente i metodi da java.lang.Object", Mark Roulo spiega come clonare gli oggetti e come ottenere una copia superficiale invece di una copia profonda. Per riassumere brevemente qui, una copia superficiale si verifica quando un oggetto viene copiato senza i suoi oggetti contenuti. Per illustrare, la Figura 1 mostra un oggetto,, obj1che contiene due oggetti containedObj1e containedObj2.

Se viene eseguita una copia superficiale obj1, viene copiata ma i suoi oggetti contenuti non lo sono, come mostrato nella Figura 2.

Una copia completa si verifica quando un oggetto viene copiato insieme agli oggetti a cui si riferisce. La figura 3 mostra obj1dopo che è stata eseguita una copia completa. Non solo è obj1stato copiato, ma sono stati copiati anche gli oggetti in esso contenuti.

Se uno di questi oggetti contenuti contiene essi stessi oggetti, allora, in una copia completa, anche quegli oggetti vengono copiati e così via fino a quando l'intero grafico non viene attraversato e copiato. Ogni oggetto è responsabile della clonazione di se stesso tramite il proprio clone()metodo. Il clone()metodo predefinito , ereditato da Object, crea una copia superficiale dell'oggetto. Per ottenere una copia completa, è necessario aggiungere una logica aggiuntiva che richiami esplicitamente tutti i clone()metodi degli oggetti contenuti , che a loro volta chiamano i clone()metodi degli oggetti contenuti e così via. Ottenere questo corretto può essere difficile e richiede tempo, ed è raramente divertente. Per rendere le cose ancora più complicate, se un oggetto non può essere modificato direttamente e il suo clone()metodo produce una copia superficiale, allora la classe deve essere estesa, ilclone()metodo ignorato e questa nuova classe utilizzata al posto della vecchia. (Ad esempio, Vectornon contiene la logica necessaria per una copia completa.) E se vuoi scrivere codice che rimanda fino al runtime la domanda se fare una copia profonda o superficiale di un oggetto, sei pronto per una soluzione ancora più complicata situazione. In questo caso, devono esserci due funzioni di copia per ogni oggetto: una per una copia profonda e una per una copia superficiale. Infine, anche se l'oggetto che viene copiato in profondità contiene più riferimenti a un altro oggetto, quest'ultimo dovrebbe comunque essere copiato solo una volta. Ciò impedisce la proliferazione di oggetti e scongiura la situazione speciale in cui un riferimento circolare produce un ciclo infinito di copie.

Serializzazione

Già nel gennaio del 1998, JavaWorld ha avviato la sua rubrica JavaBeans di Mark Johnson con un articolo sulla serializzazione, "Fallo alla maniera 'Nescafé' - con JavaBeans liofilizzati". Per riassumere, la serializzazione è la capacità di trasformare un grafico di oggetti (incluso il caso degenere di un singolo oggetto) in un array di byte che può essere trasformato in un grafico equivalente di oggetti. Si dice che un oggetto è serializzabile se esso o uno dei suoi antenati implementa java.io.Serializableo java.io.Externalizable. Un oggetto serializzabile può essere serializzato passandolo al writeObject()metodo di un ObjectOutputStreamoggetto. In questo modo vengono scritti i tipi di dati primitivi, gli array, le stringhe e altri riferimenti agli oggetti dell'oggetto. IlwriteObject()viene quindi chiamato il metodo sugli oggetti di riferimento per serializzarli. Inoltre, ciascuno di questi oggetti ha i propri riferimenti e oggetti serializzati; questo processo continua fino a quando l'intero grafico non viene attraversato e serializzato. Questo suona familiare? Questa funzionalità può essere utilizzata per ottenere una copia completa.

Copia profonda utilizzando la serializzazione

I passaggi per creare una copia completa utilizzando la serializzazione sono:

  1. Assicurati che tutte le classi nel grafico dell'oggetto siano serializzabili.

  2. Crea flussi di input e output.

  3. Utilizzare i flussi di input e output per creare flussi di input e output di oggetti.

  4. Passa l'oggetto che desideri copiare nel flusso di output dell'oggetto.

  5. Leggere il nuovo oggetto dal flusso di input dell'oggetto e restituirlo alla classe dell'oggetto inviato.

Ho scritto una classe chiamata ObjectClonerche implementa i passaggi da due a cinque. La linea contrassegnata con "A" imposta un ByteArrayOutputStreamche viene utilizzato per creare la ObjectOutputStreamlinea B. La linea C è dove viene eseguita la magia. Il writeObject()metodo attraversa ricorsivamente il grafico dell'oggetto, genera un nuovo oggetto in formato byte e lo invia a ByteArrayOutputStream. La riga D garantisce che l'intero oggetto sia stato inviato. Il codice sulla riga E crea quindi un ByteArrayInputStreame lo popola con il contenuto del file ByteArrayOutputStream. La riga F istanzia un ObjectInputStreamutilizzando il ByteArrayInputStreamcreato sulla riga E e l'oggetto viene deserializzato e restituito al metodo chiamante sulla riga G. Ecco il codice:

import java.io. *; import java.util. *; import java.awt. *; public class ObjectCloner {// in modo che nessuno possa creare accidentalmente un oggetto ObjectCloner private ObjectCloner () {} // restituisce una copia completa di un oggetto static public Object deepCopy (Object oldObj) genera un'eccezione {ObjectOutputStream oos = null; ObjectInputStream ois = null; prova {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // serializza e passa l'oggetto oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = new ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // restituisce il nuovo oggetto return ois.readObject (); // G} catch (eccezione e) {System.out.println ("Eccezione in ObjectCloner =" + e); lanciare (e); } infine {oos.close (); ois.close (); }}}

Tutto ciò che uno sviluppatore con accesso deve ObjectClonerfare prima di eseguire questo codice è assicurarsi che tutte le classi nel grafico dell'oggetto siano serializzabili. Nella maggior parte dei casi, questo avrebbe dovuto essere già stato fatto; in caso contrario, dovrebbe essere relativamente facile accedere al codice sorgente. La maggior parte delle classi nel JDK sono serializzabili; solo quelli che dipendono dalla piattaforma, come FileDescriptor, non lo sono. Inoltre, tutte le classi ottenute da un fornitore di terze parti conformi a JavaBean sono per definizione serializzabili. Ovviamente, se estendi una classe serializzabile, anche la nuova classe è serializzabile. Con tutte queste classi serializzabili che fluttuano, è probabile che le uniche che potresti aver bisogno di serializzare siano le tue, e questo è un gioco da ragazzi rispetto al passare attraverso ogni classe e sovrascrivereclone() per fare una copia approfondita.

Un modo semplice per scoprire se ci sono classi non serializzabili nel grafo di un oggetto è assumere che siano tutte serializzabili ed eseguire ObjectCloneril deepCopy()metodo su di esso. Se c'è un oggetto la cui classe non è serializzabile, java.io.NotSerializableExceptionverrà lanciato un, che ti dice quale classe ha causato il problema.

Di seguito viene mostrato un rapido esempio di implementazione. Crea un oggetto semplice,, v1che è un Vectorche contiene un file Point. Questo oggetto viene quindi stampato per mostrarne il contenuto. L'oggetto originale v1, viene quindi copiato in un nuovo oggetto vNew, che viene stampato per mostrare che contiene lo stesso valore di v1. Successivamente, il contenuto di v1viene modificato e infine vengono stampati entrambi v1e in vNewmodo che i loro valori possano essere confrontati.

import java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {try {// get the method from the command line String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Usage: java Driver1 [deep, shallow]"); ritorno; } // crea l'oggetto originale Vector v1 = new Vector (); Punto p1 = nuovo Punto (1,1); v1.addElement (p1); // guarda cos'è System.out.println ("Original =" + v1); Vector vNew = null; if (meth.equals ("deep")) {// deep copy vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("shallow")) {// shallow copy vNew = (Vector) v1.clone (); // B} // verifica che sia lo stesso System.out.println ("New =" + vNew);// cambia il contenuto dell'oggetto originale p1.x = 2; p1.y = 2; // guarda cosa c'è in ognuno ora System.out.println ("Original =" + v1); System.out.println ("New =" + vNew); } catch (Eccezione e) {System.out.println ("Eccezione in main =" + e); }}}

Per richiamare la copia completa (riga A), eseguire java.exe Driver1 deep. Quando viene eseguita la copia completa, otteniamo la seguente stampa:

Originale = [java.awt.Point [x = 1, y = 1]] Nuovo = [java.awt.Point [x = 1, y = 1]] Originale = [java.awt.Point [x = 2, y = 2]] Nuovo = [java.awt.Point [x = 1, y = 1]] 

Questo dimostra che quando l'originale Point, p1è stato cambiato, la nuova Pointcreata come risultato della copia profonda rimasti inalterati, poiché l'intero grafico è stato copiato. Per confronto, invoca la copia superficiale (riga B) eseguendo java.exe Driver1 shallow. Quando viene eseguita la copia superficiale, otteniamo la seguente stampa:

Originale = [java.awt.Point [x = 1, y = 1]] Nuovo = [java.awt.Point [x = 1, y = 1]] Originale = [java.awt.Point [x = 2, y = 2]] Nuovo = [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'implementazione di una copia completa di un oggetto grafico complesso può essere un compito difficile. La tecnica mostrata sopra è una semplice alternativa alla procedura convenzionale di sovrascrittura del clone()metodo per ogni oggetto nel grafico.

Dave Miller è un architetto senior con la società di consulenza Javelin Technology, dove lavora su applicazioni Java e Internet. Ha lavorato per aziende come Hughes, IBM, Nortel e MCIWorldcom su progetti orientati agli oggetti e ha lavorato esclusivamente con Java negli ultimi tre anni.

Ulteriori informazioni su questo argomento

  • Il sito Web Java di Sun ha una sezione dedicata alla specifica di serializzazione degli oggetti Java

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

Questa storia, "Java Tip 76: Un'alternativa alla tecnica di copia profonda" è stata originariamente pubblicata da JavaWorld.