Jak zrobić głęboką kopię obiektu w Javie

1. Wstęp

Kiedy chcemy skopiować obiekt w Javie, musimy wziąć pod uwagę dwie możliwości - płytką kopię i głęboką kopię.

Płytka kopia to podejście, w którym kopiujemy tylko wartości pól, a zatem kopia może być zależna od oryginalnego obiektu. W podejściu do głębokiego kopiowania upewniamy się, że wszystkie obiekty w drzewie są głęboko kopiowane, więc kopia nie jest zależna od żadnego wcześniej istniejącego obiektu, który może kiedykolwiek ulec zmianie.

W tym artykule porównamy te dwa podejścia i nauczymy się czterech metod implementacji głębokiej kopii.

2. Konfiguracja Mavena

Użyjemy trzech zależności Mavena - Gson, Jackson i Apache Commons Lang - do przetestowania różnych sposobów wykonywania głębokiej kopii.

Dodajmy te zależności do naszego 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 

Najnowsze wersje Gson, Jackson i Apache Commons Lang można znaleźć na Maven Central.

3. Model

Aby porównać różne metody kopiowania obiektów Java, będziemy potrzebować dwóch klas do pracy:

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. Płytka kopia

Płytka kopia to taka, w której kopiujemy tylko wartości pól z jednego obiektu do drugiego:

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

W tym przypadku pm! = ShallowCopy , co oznacza, że są to różne obiekty, ale problem polega na tym, że gdy zmienimy którąkolwiek z właściwości adresu pierwotnego , wpłynie to również na adres shallowCopy .

Nie przejmowalibyśmy się tym, gdyby adres był niezmienny, ale tak nie jest:

@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. Głęboka kopia

Głęboka kopia to alternatywa, która rozwiązuje ten problem. Jego zaletą jest to, że przynajmniej każdy zmienny obiekt w grafie obiektów jest rekurencyjnie kopiowany .

Ponieważ kopia nie jest zależna od żadnego modyfikowalnego obiektu, który został utworzony wcześniej, nie zostanie zmodyfikowany przypadkowo, tak jak widzieliśmy w przypadku płytkiej kopii.

W kolejnych sekcjach pokażemy kilka implementacji głębokiego kopiowania i zademonstrujemy tę zaletę.

5.1. Copy Constructor

Pierwsza implementacja, którą zaimplementujemy, bazuje na konstruktorach kopiujących:

public Address(Address that) { this(that.getStreet(), that.getCity(), that.getCountry()); }
public User(User that) { this(that.getFirstName(), that.getLastName(), new Address(that.getAddress())); }

W powyższej implementacji głębokiej kopii nie utworzyliśmy nowych ciągów znaków w naszym konstruktorze kopiującym, ponieważ String jest niezmienną klasą.

W efekcie nie można ich przypadkowo zmodyfikować. Zobaczmy, czy to zadziała:

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

Druga implementacja jest oparta na metodzie clone dziedziczonej z Object . Jest chroniony, ale musimy go zastąpić jako publiczny .

Dodamy również interfejs znacznika, Cloneable, do klas, aby wskazać, że klasy są faktycznie klonowalne.

Dodajmy metodę clone () do klasy Address :

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

A teraz zaimplementujmy clone () dla klasy 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; }

Zwróć uwagę, że wywołanie super.clone () zwraca płytką kopię obiektu, ale ręcznie ustawiamy głębokie kopie zmiennych zmiennych, więc wynik jest poprawny:

@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. Biblioteki zewnętrzne

The above examples look easy, but sometimes they don't apply as a solution when we can't add an additional constructor or override the clone method.

This might happen when we don't own the code, or when the object graph is so complicated that we wouldn't finish our project on time if we focused on writing additional constructors or implementing the clone method on all classes in the object graph.

What then? In this case, we can use an external library. To achieve a deep copy, we can serialize an object and then deserialize it to a new object.

Let's look at a few examples.

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. Serializacja JSON z Jacksonem

Jackson to kolejna biblioteka obsługująca serializację JSON. Ta implementacja będzie bardzo podobna do tej używającej Gson, ale musimy dodać domyślny konstruktor do naszych klas .

Zobaczmy przykład:

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

Jakiej implementacji powinniśmy użyć, wykonując głęboką kopię? Ostateczna decyzja będzie często zależała od klas, które będziemy kopiować i od tego, czy jesteśmy właścicielami klas w grafie obiektów.

Jak zawsze, pełne przykłady kodu dla tego samouczka można znaleźć na GitHub.