Перейти к основному содержимому

Как сделать глубокую копию объекта в Java

· 6 мин. чтения

1. Введение

Когда мы хотим скопировать объект в Java, нам нужно рассмотреть две возможности: поверхностную копию и глубокую копию .

При поверхностном копировании мы копируем только значения полей, поэтому копия может зависеть от исходного объекта. В подходе глубокого копирования мы гарантируем, что все объекты в дереве глубоко скопированы, поэтому копия не зависит от какого-либо ранее существовавшего объекта, который может когда-либо измениться.

В этом руководстве мы сравним эти два подхода и изучим четыре метода реализации глубокого копирования.

2. Настройка Мавена

Мы будем использовать три зависимости Maven, Gson, Jackson и Apache Commons Lang, чтобы протестировать различные способы выполнения глубокого копирования.

Давайте добавим эти зависимости в наш pom.xml :

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>

Последние версии Gson , Jackson и Apache Commons Lang можно найти на Maven Central.

3. Модель

Чтобы сравнить различные методы копирования объектов Java, нам понадобятся два класса для работы:

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. Поверхностное копирование

Неглубокая копия — это копия, в которой мы копируем только значения полей из одного объекта в другой:

@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);
}

В данном случае pm != smallCopy означает, что это разные объекты; однако проблема в том, что когда мы меняем любое из свойств исходного адреса , это также повлияет на адрес мелкой копии .

Мы бы не беспокоились об этом, если бы Address был неизменным, но это не так:

@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. Глубокое копирование

Глубокая копия является альтернативой, которая решает эту проблему. Его преимущество в том, что каждый изменяемый объект в графе объектов рекурсивно копируется .

Поскольку копия не зависит ни от какого изменяемого объекта, который был создан ранее, она не будет изменена случайно, как мы видели с поверхностной копией.

В следующих разделах мы обсудим несколько реализаций глубокого копирования и продемонстрируем это преимущество.

5.1. Копировать конструктор

Первая реализация, которую мы рассмотрим, основана на конструкторах копирования:

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

В приведенной выше реализации глубокой копии мы не создавали новые строки в нашем конструкторе копирования, потому что String является неизменяемым классом.

В результате они не могут быть изменены случайно. Посмотрим, работает ли это:

@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. Клонируемый интерфейс

Вторая реализация основана на методе clone, унаследованном от Object . Он защищен, но нам нужно переопределить его как общедоступный .

Мы также добавим маркерный интерфейс Cloneable к классам, чтобы указать, что классы действительно можно клонировать.

Давайте добавим метод clone() в класс Address :

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

Теперь давайте реализуем clone() для класса User :

@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;
}

Обратите внимание, что вызов super.clone() возвращает поверхностную копию объекта, но мы устанавливаем глубокие копии изменяемых полей вручную, поэтому результат правильный:

@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. Внешние библиотеки

Приведенные выше примеры кажутся простыми, но иногда они не работают как решение, когда мы не можем добавить дополнительный конструктор или переопределить метод клонирования .

Это может случиться, когда мы не владеем кодом или когда граф объектов настолько сложен, что мы не смогли бы завершить свой проект вовремя, если бы сосредоточились на написании дополнительных конструкторов или реализации метода клонирования для всех классов в графе объектов.

Так что же мы можем сделать тогда? В этом случае мы можем использовать внешнюю библиотеку. Чтобы получить глубокую копию, мы можем сериализовать объект, а затем десериализовать его в новый объект .

Давайте рассмотрим несколько примеров.

6.1. Apache Commons Ланг

В Apache Commons Lang есть SerializationUtils#clone, который выполняет глубокое копирование, когда все классы в графе объектов реализуют интерфейс Serializable .

Если метод встретит несериализуемый класс, он завершится ошибкой и выдаст непроверенное исключение SerializationException .

Следовательно, нам нужно добавить в наши классы интерфейс Serializable :

@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 с помощью Gson

Другой способ сериализации — использовать сериализацию JSON. Gson — это библиотека, которая используется для преобразования объектов в JSON и наоборот.

В отличие от Apache Commons Lang, GSON не нуждается в интерфейсе Serializable для выполнения преобразований .

Давайте быстро рассмотрим пример:

@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. Сериализация JSON с Джексоном

Jackson — еще одна библиотека, поддерживающая сериализацию JSON. Эта реализация будет очень похожа на ту, что использует Gson, но нам нужно добавить в наши классы конструктор по умолчанию .

Давайте посмотрим пример:

@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. Заключение

Какую реализацию мы должны использовать при создании глубокой копии? Окончательное решение часто будет зависеть от того, какие классы мы будем копировать, и от того, владеем ли мы классами в графе объектов.

Как всегда, полные примеры кода для этой статьи можно найти на GitHub .