Cum se face o copie profundă a unui obiect în Java

1. Introducere

Când vrem să copiem un obiect în Java, trebuie să luăm în considerare două posibilități - o copie superficială și o copie profundă.

Copia superficială este abordarea atunci când copiem doar valorile câmpului și, prin urmare, copia ar putea fi dependentă de obiectul original. În abordarea copierii profunde, ne asigurăm că toate obiectele din copac sunt copiate profund, astfel încât copia nu depinde de niciun obiect existent anterior care s-ar putea schimba vreodată.

În acest articol, vom compara aceste două abordări și vom învăța patru metode pentru a implementa copia profundă.

2. Configurare Maven

Vom folosi trei dependențe Maven - Gson, Jackson și Apache Commons Lang - pentru a testa diferite moduri de a efectua o copie profundă.

Să adăugăm aceste dependențe la 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 

Cele mai recente versiuni ale lui Gson, Jackson și Apache Commons Lang pot fi găsite pe Maven Central.

3. Model

Pentru a compara diferite metode de copiere a obiectelor Java, vom avea nevoie de două clase pentru a lucra:

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. Copiere superficială

O copie superficială este una în care copiem doar valorile câmpurilor de la un obiect la altul:

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

În acest caz pm! = ShallowCopy , ceea ce înseamnă că sunt obiecte diferite, dar problema este că atunci când modificăm oricare dintre proprietățile adresei originale , aceasta va afecta și adresa shallowCopy .

Nu ne-am deranja dacă Adresa ar fi imuabilă, dar nu este:

@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. Copiere profundă

O copie profundă este o alternativă care rezolvă această problemă. Avantajul său este că cel puțin fiecare obiect mutabil din graficul obiectului este copiat recursiv .

Deoarece copia nu depinde de niciun obiect mutabil care a fost creat anterior, nu va fi modificat accidental, așa cum am văzut cu copia superficială.

În secțiunile următoare, vom arăta mai multe implementări de copiere profundă și vom demonstra acest avantaj.

5.1. Copiați constructorul

Prima implementare pe care o vom implementa se bazează pe constructori de copii:

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

În implementarea de mai sus a copiei profunde, nu am creat șiruri noi în constructorul nostru de copii, deoarece String este o clasă imuabilă.

Ca urmare, nu pot fi modificate accidental. Să vedem dacă funcționează:

@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. Interfață clonabilă

A doua implementare se bazează pe metoda clonării moștenită de la Object . Este protejat, dar trebuie să îl anulăm ca public .

Vom adăuga, de asemenea, o interfață de marker, Clonabilă, la clase pentru a indica faptul că clasele sunt de fapt clonabile.

Să adăugăm metoda clone () la clasa Address :

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

Și acum să implementăm clone () pentru clasa 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; }

Rețineți că apelul super.clone () returnează o copie superficială a unui obiect, dar setăm manual copii adânci ale câmpurilor modificabile, astfel încât rezultatul este corect:

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

Exemplele de mai sus arată ușor, dar uneori nu se aplică ca soluție atunci când nu putem adăuga un constructor suplimentar sau nu putem înlocui metoda clonării .

Acest lucru s-ar putea întâmpla atunci când nu deținem codul sau când graficul obiectului este atât de complicat încât nu ne-am termina proiectul la timp dacă ne-am concentra pe scrierea unor constructori suplimentari sau implementarea metodei clonării pe toate clasele din graficul obiect.

Ce atunci? În acest caz, putem folosi o bibliotecă externă. Pentru a realiza o copie profundă, putem serializa un obiect și apoi îl putem deserializa într-un obiect nou .

Să ne uităm la câteva exemple.

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. Serializare JSON cu Jackson

Jackson este o altă bibliotecă care acceptă serializarea JSON. Această implementare va fi foarte asemănătoare cu cea care folosește Gson, dar trebuie să adăugăm constructorul implicit la clasele noastre .

Să vedem un exemplu:

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

Ce implementare ar trebui să folosim atunci când facem o copie profundă? Decizia finală va depinde adesea de clasele pe care le vom copia și de dacă deținem clasele în graficul obiectelor.

Ca întotdeauna, mostrele complete de cod pentru acest tutorial pot fi găsite pe GitHub.