Рубрики
Без рубрики

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

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

Автор оригинала: Adam Gurgul.

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

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

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

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

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

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

@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() метод в Адрес класс:

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

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

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

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

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

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

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

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

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

6.1. Язык Apache Commons Lang

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

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

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

@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 С Помощью Json

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

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

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

@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, но нам нужно добавить конструктор по умолчанию в наши классы .

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

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