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

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

1. Вступление

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

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

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

2. Maven Setup

Мы будем использовать три зависимости 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. Мелкая копия

Неглубокая копия - это такая, в которойwe only copy values of fields от одного объекта к другому:

@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 != shallowCopy, что означает, чтоthey’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.

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

Глубокая копия является альтернативой, которая решает эту проблему. Его преимущество в том, что не менееeach mutable object in the object graph is recursively copied.

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

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

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

В приведенной выше реализации глубокой копии мы не создали новыйStrings в нашем конструкторе копирования, потому что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. Клонируемый интерфейс

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

Мы также добавим к классам интерфейс маркера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. Внешние библиотеки

Приведенные выше примеры выглядят простыми, но иногда они не подходят для решенияwhen we can’t add an additional constructor or override the clone method.

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

Что тогда? В этом случае мы можем использовать внешнюю библиотеку. Чтобы получить глубокую копию,we can serialize an object and then deserialize it to a new object.

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

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 does not need the Serializable interface to make the conversions.

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

@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 с Джексоном

Джексон - еще одна библиотека, которая поддерживает сериализацию JSON. Эта реализация будет очень похожа на реализацию с использованием Gson, ноwe need to add the default constructor to our classes.

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

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

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

Как всегда, полные образцы кода для этого руководства можно найти вover on Github.