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

1. Введение

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

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

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

2. Настройка Maven

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

Давайте добавим эти зависимости в наш 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 

Последние версии 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); }

В этом случае вечер! = ShallowCopy , что означает , что они разные объекты, но проблема в том , что , когда мы меняем любые из оригинального адрес свойств, это также будет влиять на shallowCopy «адрес s .

Мы бы не стали беспокоиться об этом, если бы 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 Lang

В 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.