Consejo 76 de Java: una alternativa a la técnica de copia profunda

Implementar una copia profunda de un objeto puede ser una experiencia de aprendizaje: ¡aprende que no quiere hacerlo! Si el objeto en cuestión se refiere a otros objetos complejos, que a su vez se refieren a otros, entonces esta tarea puede resultar desalentadora. Tradicionalmente, cada clase en el objeto debe ser inspeccionada y editada individualmente para implementar la Cloneableinterfaz y anular su clone()método con el fin de hacer una copia profunda de sí misma y de sus objetos contenidos. Este artículo describe una técnica simple para usar en lugar de esta copia profunda convencional que consume mucho tiempo.

El concepto de copia profunda

Para entender qué es una copia profunda , veamos primero el concepto de copia superficial.

En un artículo anterior de JavaWorld , "Cómo evitar trampas y anular correctamente los métodos de java.lang.Object", Mark Roulo explica cómo clonar objetos y cómo lograr una copia superficial en lugar de una copia profunda. Para resumir brevemente aquí, una copia superficial ocurre cuando un objeto se copia sin sus objetos contenidos. Para ilustrar, la Figura 1 muestra un objeto`` obj1que contiene dos objetos, containedObj1y containedObj2.

Si se realiza una copia superficial obj1, se copia, pero no los objetos que contiene, como se muestra en la Figura 2.

Una copia profunda se produce cuando se copia un objeto junto con los objetos a los que se refiere. La Figura 3 muestra obj1después de que se realizó una copia en profundidad. No solo se obj1ha copiado, sino que también se han copiado los objetos que contiene.

Si alguno de estos objetos contenidos contiene objetos, entonces, en una copia profunda, esos objetos también se copian, y así sucesivamente hasta que se recorre y se copia todo el gráfico. Cada objeto es responsable de clonarse a sí mismo mediante su clone()método. El clone()método predeterminado , heredado de Object, hace una copia superficial del objeto. Para lograr una copia profunda, se debe agregar lógica adicional que llame explícitamente a todos los clone()métodos de los objetos contenidos , que a su vez llaman a los clone()métodos de sus objetos contenidos , y así sucesivamente. Hacer esto correctamente puede ser difícil, llevar mucho tiempo y rara vez es divertido. Para hacer las cosas aún más complicadas, si un objeto no se puede modificar directamente y su clone()método produce una copia superficial, entonces la clase debe ampliarse, laclone()método reemplazado, y esta nueva clase se usa en lugar de la antigua. (Por ejemplo, Vectorno contiene la lógica necesaria para una copia profunda). Y si desea escribir código que posponga hasta el tiempo de ejecución la cuestión de si hacer una copia profunda o superficial de un objeto, se encontrará con un código aún más complicado situación. En este caso, debe haber dos funciones de copia para cada objeto: una para una copia profunda y otra para una superficial. Finalmente, incluso si el objeto que se está copiando en profundidad contiene múltiples referencias a otro objeto, este último objeto solo debe copiarse una vez. Esto evita la proliferación de objetos y evita la situación especial en la que una referencia circular produce un bucle infinito de copias.

Publicación por entregas

En enero de 1998, JavaWorld inició su columna JavaBeans de Mark Johnson con un artículo sobre serialización, "Hágalo al estilo 'Nescafé', con JavaBeans liofilizados". En resumen, la serialización es la capacidad de convertir un gráfico de objetos (incluido el caso degenerado de un solo objeto) en una matriz de bytes que se puede convertir de nuevo en un gráfico equivalente de objetos. Se dice que un objeto es serializable si él o uno de sus antepasados ​​implementa java.io.Serializableo java.io.Externalizable. Un objeto serializable se puede serializar pasándolo al writeObject()método de un ObjectOutputStreamobjeto. Esto escribe los tipos de datos primitivos, matrices, cadenas y otras referencias de objetos del objeto. loswriteObject()luego se llama al método en los objetos referidos para serializarlos también. Además, cada uno de estos objetos tiene sus referencias y objetos serializados; este proceso continúa y continúa hasta que se recorre y se serializa todo el gráfico. ¿Te suena familiar? Esta funcionalidad se puede utilizar para lograr una copia en profundidad.

Copia profunda mediante serialización

Los pasos para realizar una copia profunda mediante la serialización son:

  1. Asegúrese de que todas las clases del gráfico del objeto sean serializables.

  2. Cree flujos de entrada y salida.

  3. Utilice los flujos de entrada y salida para crear flujos de entrada y salida de objetos.

  4. Pase el objeto que desea copiar al flujo de salida del objeto.

  5. Lea el nuevo objeto del flujo de entrada del objeto y devuélvalo a la clase del objeto que envió.

Escribí una clase llamada ObjectClonerque implementa los pasos del dos al cinco. La línea marcada "A" configura una ByteArrayOutputStreamque se usa para crear la ObjectOutputStreamlínea B. La línea C es donde se hace la magia. El writeObject()método atraviesa de forma recursiva el gráfico del objeto, genera un nuevo objeto en forma de byte y lo envía al archivo ByteArrayOutputStream. La línea D asegura que se ha enviado todo el objeto. El código en la línea E luego crea un ByteArrayInputStreamy lo llena con el contenido del ByteArrayOutputStream. La línea F crea una instancia ObjectInputStreamusando el ByteArrayInputStreamcreado en la línea E y el objeto se deserializa y se devuelve al método de llamada en la línea G. Aquí está el código:

importar java.io. *; importar java.util. *; importar java.awt. *; public class ObjectCloner {// para que nadie pueda crear accidentalmente un objeto ObjectCloner private ObjectCloner () {} // devuelve una copia profunda de un objeto static public Object deepCopy (Object oldObj) throws Exception {ObjectOutputStream oos = null; ObjectInputStream ois = null; prueba {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // serializa y pasa el objeto oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = new ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // devuelve el nuevo objeto return ois.readObject (); // G} catch (Exception e) {System.out.println ("Exception in ObjectCloner =" + e); lanzar (e); } finalmente {oos.close (); ois.close (); }}}

Todo lo que tiene que hacer un desarrollador con acceso ObjectClonerantes de ejecutar este código es asegurarse de que todas las clases en el gráfico del objeto sean serializables. En la mayoría de los casos, esto ya debería haberse hecho; si no, debería ser relativamente fácil de hacer con acceso al código fuente. La mayoría de las clases del JDK son serializables; solo los que dependen de la plataforma, como, por ejemplo FileDescriptor, no. Además, cualquier clase que obtenga de un proveedor externo que sea compatible con JavaBean es, por definición, serializable. Por supuesto, si extiende una clase que es serializable, entonces la nueva clase también es serializable. Con todas estas clases serializables flotando, es probable que las únicas que necesite serializar sean las suyas, y esto es muy fácil en comparación con pasar por cada clase y sobrescribirclone() hacer una copia profunda.

Una manera fácil de averiguar si tiene clases no serializables en el gráfico de un objeto es asumir que todas son serializables y ejecutar ObjectClonerel deepCopy()método en él. Si hay un objeto cuya clase no es serializable, se java.io.NotSerializableExceptionlanzará un que le indicará qué clase causó el problema.

A continuación se muestra un ejemplo de implementación rápida. Crea un objeto simple v1, que es un Vectorque contiene un Point. A continuación, este objeto se imprime para mostrar su contenido. El objeto original,, v1se copia a un nuevo objeto vNew, que se imprime para mostrar que contiene el mismo valor que v1. A continuación, v1se cambia el contenido de , y finalmente se imprimen ambos v1y vNewpara poder comparar sus valores.

importar java.util. *; importar java.awt. *; public class Driver1 {static public void main (String [] args) {try {// obtener el método de la línea de comando String meth; if ((args.length == 1) && ((args [0] .equals ("deep")) || (args [0] .equals ("shallow")))) {meth = args [0]; } else {System.out.println ("Uso: java Driver1 [profundo, superficial]"); regreso; } // crea el objeto original Vector v1 = new Vector (); Punto p1 = nuevo Punto (1,1); v1.addElement (p1); // ver qué es System.out.println ("Original =" + v1); Vector vNew = null; if (meth.equals ("deep")) {// copia profunda vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ("superficial")) {// copia superficial vNew = (Vector) v1.clone (); // B} // verifica que sea el mismo System.out.println ("New =" + vNew);// cambia el contenido del objeto original p1.x = 2; p1.y = 2; // mira lo que hay en cada uno ahora System.out.println ("Original =" + v1); System.out.println ("Nuevo =" + vNuevo); } catch (Excepción e) {System.out.println ("Excepción en main =" + e); }}}

Para invocar la copia profunda (línea A), ejecute java.exe Driver1 deep. Cuando se ejecuta la copia profunda, obtenemos la siguiente impresión:

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

Esto demuestra que cuando el original Point, p1, fue cambiado, la nueva Pointcreada como resultado de la copia en profundidad no se vio afectado, ya que toda la gráfica se copió. A modo de comparación, invoque la copia superficial (línea B) ejecutando java.exe Driver1 shallow. Cuando se ejecuta la copia superficial, obtenemos la siguiente impresión:

Original = [java.awt.Point [x = 1, y = 1]] Nuevo = [java.awt.Point [x = 1, y = 1]] Original = [java.awt.Point [x = 2, y = 2]] Nuevo = [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

La implementación de una copia profunda de un gráfico de objeto complejo puede ser una tarea difícil. La técnica que se muestra arriba es una alternativa simple al procedimiento convencional de sobrescribir el clone()método para cada objeto en el gráfico.

Dave Miller es arquitecto senior en la consultora Javelin Technology, donde trabaja en aplicaciones Java e Internet. Ha trabajado para empresas como Hughes, IBM, Nortel y MCIWorldcom en proyectos orientados a objetos y ha trabajado exclusivamente con Java durante los últimos tres años.

Más información sobre este tema

  • El sitio web Java de Sun tiene una sección dedicada a la Especificación de serialización de objetos Java

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

Esta historia, "Consejo 76 de Java: una alternativa a la técnica de copia profunda" fue publicada originalmente por JavaWorld.