Cómo hacer una copia profunda de un objeto en Java

1. Introducción

Cuando queremos copiar un objeto en Java, hay dos posibilidades que debemos considerar: una copia superficial y una copia profunda.

La copia superficial es el enfoque cuando solo copiamos valores de campo y, por lo tanto, la copia puede depender del objeto original. En el enfoque de copia profunda, nos aseguramos de que todos los objetos del árbol se copien en profundidad, de modo que la copia no dependa de ningún objeto existente anterior que pueda cambiar.

En este artículo, compararemos estos dos enfoques y aprenderemos cuatro métodos para implementar la copia profunda.

2. Configuración de Maven

Usaremos tres dependencias de Maven (Gson, Jackson y Apache Commons Lang) para probar diferentes formas de realizar una copia en profundidad.

Agreguemos estas dependencias a nuestro pom.xml :

 com.google.code.gson gson 2.8.2   commons-lang commons-lang 2.6   com.fasterxml.jackson.core jackson-databind 2.9.3 

Las últimas versiones de Gson, Jackson y Apache Commons Lang se pueden encontrar en Maven Central.

3. Modelo

Para comparar diferentes métodos para copiar objetos Java, necesitaremos dos clases para trabajar:

class Address { private String street; private String city; private String country; // standard constructors, getters and setters }
class User { private String firstName; private String lastName; private Address address; // standard constructors, getters and setters }

4. Copia superficial

Una copia superficial es aquella en la que solo copiamos valores de campos de un objeto a otro:

@Test public void whenShallowCopying_thenObjectsShouldNotBeSame() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User shallowCopy = new User( pm.getFirstName(), pm.getLastName(), pm.getAddress()); assertThat(shallowCopy) .isNotSameAs(pm); }

En este caso pm! = ShallowCopy , lo que significa que son objetos diferentes, pero el problema es que cuando cambiamos cualquiera de las propiedades de la dirección original , esto también afectará la dirección de shallowCopy .

No nos preocuparíamos por eso si Address fuera inmutable, pero no lo es:

@Test public void whenModifyingOriginalObject_ThenCopyShouldChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User shallowCopy = new User( pm.getFirstName(), pm.getLastName(), pm.getAddress()); address.setCountry("Great Britain"); assertThat(shallowCopy.getAddress().getCountry()) .isEqualTo(pm.getAddress().getCountry()); }

5. Copia profunda

Una copia profunda es una alternativa que resuelve este problema. Su ventaja es que al menos cada objeto mutable en el gráfico de objetos se copia de forma recursiva .

Dado que la copia no depende de ningún objeto mutable que se creó anteriormente, no se modificará por accidente como vimos con la copia superficial.

En las siguientes secciones, mostraremos varias implementaciones de copia profunda y demostraremos esta ventaja.

5.1. Copiar constructor

La primera implementación que implementaremos se basa en constructores de copia:

public Address(Address that) { this(that.getStreet(), that.getCity(), that.getCountry()); }
public User(User that) { this(that.getFirstName(), that.getLastName(), new Address(that.getAddress())); }

En la implementación anterior de la copia profunda, no hemos creado nuevos Strings en nuestro constructor de copia porque String es una clase inmutable.

Como resultado, no se pueden modificar por accidente. Vamos a ver si esto funciona:

@Test public void whenModifyingOriginalObject_thenCopyShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User deepCopy = new User(pm); address.setCountry("Great Britain"); assertNotEquals( pm.getAddress().getCountry(), deepCopy.getAddress().getCountry()); }

5.2. Interfaz clonable

La segunda implementación se basa en el método de clonación heredado de Object . Está protegido, pero debemos anularlo como público .

También agregaremos una interfaz de marcador, Cloneable, a las clases para indicar que las clases son realmente clonables.

Agreguemos el método clone () a la clase Address :

@Override public Object clone() { try { return (Address) super.clone(); } catch (CloneNotSupportedException e) { return new Address(this.street, this.getCity(), this.getCountry()); } }

Y ahora implementemos clone () para la clase de usuario :

@Override public Object clone() { User user = null; try { user = (User) super.clone(); } catch (CloneNotSupportedException e) { user = new User( this.getFirstName(), this.getLastName(), this.getAddress()); } user.address = (Address) this.address.clone(); return user; }

Tenga en cuenta que la llamada super.clone () devuelve una copia superficial de un objeto, pero establecemos copias profundas de campos mutables manualmente, por lo que el resultado es correcto:

@Test public void whenModifyingOriginalObject_thenCloneCopyShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User deepCopy = (User) pm.clone(); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

6. Bibliotecas externas

The above examples look easy, but sometimes they don't apply as a solution when we can't add an additional constructor or override the clone method.

This might happen when we don't own the code, or when the object graph is so complicated that we wouldn't finish our project on time if we focused on writing additional constructors or implementing the clone method on all classes in the object graph.

What then? In this case, we can use an external library. To achieve a deep copy, we can serialize an object and then deserialize it to a new object.

Let's look at a few examples.

6.1. Apache Commons Lang

Apache Commons Lang has SerializationUtils#clone, which performs a deep copy when all classes in the object graph implement the Serializable interface.

If the method encounters a class that isn't serializable, it'll fail and throw an unchecked SerializationException.

Because of that, we need to add the Serializable interface to our classes:

@Test public void whenModifyingOriginalObject_thenCommonsCloneShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User deepCopy = (User) SerializationUtils.clone(pm); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

6.2. JSON Serialization With Gson

The other way to serialize is to use JSON serialization. Gson is a library that's used for converting objects into JSON and vice versa.

Unlike Apache Commons Lang, GSON does not need the Serializable interface to make the conversions.

Let's have a quick look at an example:

@Test public void whenModifyingOriginalObject_thenGsonCloneShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); Gson gson = new Gson(); User deepCopy = gson.fromJson(gson.toJson(pm), User.class); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

6.3. Serialización JSON con Jackson

Jackson es otra biblioteca que admite la serialización JSON. Esta implementación será muy similar a la que usa Gson, pero necesitamos agregar el constructor predeterminado a nuestras clases .

Veamos un ejemplo:

@Test public void whenModifyingOriginalObject_thenJacksonCopyShouldNotChange() throws IOException { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); ObjectMapper objectMapper = new ObjectMapper(); User deepCopy = objectMapper .readValue(objectMapper.writeValueAsString(pm), User.class); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

7. Conclusión

¿Qué implementación deberíamos usar al hacer una copia profunda? La decisión final a menudo dependerá de las clases que copiemos y de si somos dueños de las clases en el gráfico de objetos.

Como siempre, las muestras de código completas para este tutorial se pueden encontrar en GitHub.