Como fazer uma cópia profunda de um objeto em Java
1. Introdução
Quando queremos copiar um objeto em Java, há duas possibilidades que precisamos considerar - uma cópia superficial e uma cópia profunda.
A cópia superficial é a abordagem quando copiamos apenas os valores dos campos e, portanto, a cópia pode depender do objeto original. Na abordagem de cópia profunda, certificamo-nos de que todos os objetos na árvore são copiados profundamente, para que a cópia não dependa de qualquer objeto existente anterior que possa mudar.
Neste artigo, compararemos essas duas abordagens e aprenderemos quatro métodos para implementar a cópia profunda.
2. Configuração do Maven
Usaremos três dependências Maven - Gson, Jackson e Apache Commons Lang - para testar diferentes maneiras de realizar uma cópia profunda.
Vamos adicionar essas dependências ao nossopom.xml:
com.google.code.gson
gson
2.8.2
commons-lang
commons-lang
2.6
com.fasterxml.jackson.core
jackson-databind
2.9.3
As versões mais recentes deGson,Jackson eApache Commons Lang podem ser encontradas no Maven Central.
3. Modelo
Para comparar diferentes métodos de cópia de objetos Java, precisaremos de duas classes para trabalhar:
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. Cópia rasa
Uma cópia superficial é aquela em quewe only copy values of fields de um objeto para outro:
@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);
}
Nesse caso,pm != shallowCopy, o que significa quethey’re different objects, but the problem is that when we change any of the original address' properties, this will also affect the shallowCopy‘s address.
Não nos importaríamos com isso seAddress fosse imutável, mas não é:
@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. Cópia profunda
Uma cópia profunda é uma alternativa que resolve esse problema. Sua vantagem é que pelo menoseach mutable object in the object graph is recursively copied.
Uma vez que a cópia não depende de nenhum objeto mutável que foi criado anteriormente, ela não será modificada por acidente, como vimos com a cópia superficial.
Nas seções a seguir, mostraremos várias implementações de cópia profunda e demonstraremos essa vantagem.
5.1. Copiar Construtor
A primeira implementação que implementaremos é baseada em construtores de cópia:
public Address(Address that) {
this(that.getStreet(), that.getCity(), that.getCountry());
}
public User(User that) {
this(that.getFirstName(), that.getLastName(), new Address(that.getAddress()));
}
Na implementação acima da cópia profunda, não criamos novoStrings em nosso construtor de cópia porqueString é uma classe imutável.
Como resultado, eles não podem ser modificados por acidente. Vamos ver se isso 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. Interface clonável
A segunda implementação é baseada no método clone herdado deObject. Está protegido, mas precisamos substituí-lo comopublic.
Também adicionaremos uma interface de marcador,Cloneable, às classes para indicar que as classes são realmente clonáveis.
Vamos adicionar o métodoclone() à classeAddress:
@Override
public Object clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
return new Address(this.street, this.getCity(), this.getCountry());
}
}
E agora vamos implementarclone() para a classeUser:
@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;
}
Observe que a chamadasuper.clone() retorna uma cópia superficial de um objeto, mas definimos cópias profundas de campos mutáveis manualmente, então o resultado está correto:
@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
Os exemplos acima parecem fáceis, mas às vezes eles não se aplicam como uma soluçãowhen we can’t add an additional constructor or override the clone method.
Isso pode acontecer quando não somos donos do código, ou quando o gráfico do objeto é tão complicado que não terminaríamos nosso projeto a tempo se nos concentrássemos em escrever construtores adicionais ou implementar o métodoclone em todas as classes em o gráfico do objeto.
O que então? Nesse caso, podemos usar uma biblioteca externa. Para obter uma cópia profunda,we can serialize an object and then deserialize it to a new object.
Vejamos alguns exemplos.
6.1. Apache Commons Lang
O Apache Commons Lang temSerializationUtils#clone, que executa uma cópia profunda quando todas as classes no gráfico de objeto implementam a interfaceSerializable.
Se o método encontrar uma classe que não seja serializável, ele falhará e lançará umSerializationException desmarcado.
Por isso, precisamos adicionar a interfaceSerializable às nossas 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. Serialização JSON com Gson
A outra maneira de serializar é usar a serialização JSON. Gson é uma biblioteca usada para converter objetos em JSON e vice-versa.
Ao contrário do Apache Commons Lang,GSON does not need the Serializable interface to make the conversions.
Vamos dar uma rápida olhada em um exemplo:
@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. Serialização JSON com Jackson
Jackson é outra biblioteca que suporta serialização JSON. Esta implementação será muito semelhante à que usa Gson, maswe need to add the default constructor to our classes.
Vamos ver um exemplo:
@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. Conclusão
Qual implementação devemos usar ao fazer uma cópia profunda? A decisão final frequentemente dependerá das classes que iremos copiar e se possuímos as classes no gráfico do objeto.
Como sempre, os exemplos de código completos para este tutorial podem ser encontradosover on Github.