Как да направя дълбоко копие на обект в 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 " адрес и .

Не бихме се притеснявали, ако Адресът е неизменен, но не е:

@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. Cloneable интерфейс

Втората реализация се основава на метода на клониране, наследен от 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 () за потребителския клас:

@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 #, който изпълнява дълбоко копиране, когато всички класове в обектната графика реализират интерфейса 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 не се нуждае от сериализуем интерфейс за извършване на преобразуванията .

Нека да разгледаме набързо един пример:

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