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 .
Ми б не турбувались про це, якби Адреса була незмінною, але це не так:
@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. Клонуючий інтерфейс
Друга реалізація заснована на методі клонування, успадкованому від 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. Зовнішні бібліотеки
Наведені вище приклади виглядають легко, але іноді вони не застосовуються як рішення, коли ми не можемо додати додатковий конструктор або замінити метод клонування .
Це може трапитися, коли ми не володіємо кодом, або коли графік об’єкта настільки складний, що ми не закінчимо наш проект вчасно, якщо зосередимося на написанні додаткових конструкторів або реалізації методу clone на всіх класах в графі об’єктів.
Що тоді? У цьому випадку ми можемо використовувати зовнішню бібліотеку. Щоб отримати глибоку копію, ми можемо серіалізувати об’єкт, а потім десеріалізувати його до нового об’єкта .
Давайте розглянемо кілька прикладів.
6.1. Apache Commons Lang
Apache Commons Lang has SerializationUtils#clone, which performs a deep copy when all classes in the object graph implement the Serializable interface.
If the method encounters a class that isn't serializable, it'll fail and throw an unchecked SerializationException.
Because of that, we need to add the Serializable interface to our 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. JSON Serialization With Gson
The other way to serialize is to use JSON serialization. Gson is a library that's used for converting objects into JSON and vice versa.
Unlike Apache Commons Lang, GSON does not need the Serializable interface to make the conversions.
Let's have a quick look at an example:
@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.