Hvordan lage en dyp kopi av et objekt i Java

1. Introduksjon

Når vi vil kopiere et objekt i Java, er det to muligheter vi må vurdere - en grunne kopi og en dyp kopi.

Den grunne kopien er tilnærmingen når vi bare kopierer feltverdier, og kopien kan derfor være avhengig av det originale objektet. I dypkopi-tilnærmingen sørger vi for at alle gjenstandene i treet kopieres dypt, slik at kopien ikke er avhengig av noe tidligere eksisterende objekt som noen gang kan endres.

I denne artikkelen vil vi sammenligne disse to tilnærmingene og lære fire metoder for å implementere den dype kopien.

2. Maven-oppsett

Vi bruker tre Maven-avhengigheter - Gson, Jackson og Apache Commons Lang - for å teste forskjellige måter å utføre en dyp kopi på.

La oss legge til disse avhengighetene til vår 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 

De nyeste versjonene av Gson, Jackson og Apache Commons Lang finnes på Maven Central.

3. Modell

For å sammenligne forskjellige metoder for å kopiere Java-objekter, trenger vi to klasser å jobbe med:

klasse Adresse {privat String street; private String city; privat strengland; // standardkonstruktører, getters og settere}
klasse bruker {privat streng fornavn; privat streng etternavn; privat adresse adresse; // standardkonstruktører, getters og settere}

4. Grunn kopi

En grunne kopi er en der vi kopierer bare verdier av felt fra ett objekt til et annet:

@Test offentlig ugyldig nårShallowCopying_thenObjectsShouldNotBeSame () {Address address = new Address ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); Bruker shallowCopy = ny bruker (pm.getFirstName (), pm.getLastName (), pm.getAddress ()); assertThat (shallowCopy) .isNotSameAs (pm); }

I dette tilfellet pm! = grunne kopi, som betyr at de er forskjellige gjenstander, men problemet er at når vi endrer noe av originalen adresse' egenskaper, vil dette også påvirke grunne kopi’S adresse.

Vi ville ikke bry oss om det hvis Adresse var uforanderlig, men det er ikke:

@Test offentlig ugyldig nårModifyingOriginalObject_ThenCopyShouldChange () {Adresse adresse = ny adresse ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); Bruker shallowCopy = ny bruker (pm.getFirstName (), pm.getLastName (), pm.getAddress ()); address.setCountry ("Storbritannia"); assertThat (shallowCopy.getAddress (). getCountry ()) .isEqualTo (pm.getAddress (). getCountry ()); }

5. Dyp kopi

En dyp kopi er et alternativ som løser dette problemet. Dens fordel er at i det minste hvert mutable objekt i objektgrafen kopieres rekursivt.

Siden kopien ikke er avhengig av noe foranderlig objekt som ble opprettet tidligere, blir den ikke modifisert ved et uhell som vi så med den grunne kopien.

I de følgende avsnittene viser vi flere implementeringer av dype kopier og demonstrerer denne fordelen.

5.1. Copy Constructor

Den første implementeringen vi implementerer er basert på kopikonstruktører:

public Address (Address that) {this (that.getStreet (), that.getCity (), that.getCountry ()); }
offentlig bruker (bruker som) {dette (that.getFirstName (), that.getLastName (), ny adresse (that.getAddress ())); }

I implementeringen ovenfor av den dype kopien har vi ikke opprettet nye Strenger i kopikonstruktøren vår fordi String er en uforanderlig klasse.

Som et resultat kan de ikke endres ved et uhell. La oss se om dette fungerer:

@Test offentlig ugyldig nårModifyingOriginalObject_thenCopyShouldNotChange () {Address address = new Address ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); User deepCopy = ny bruker (pm); address.setCountry ("Storbritannia"); assertNotEquals (pm.getAddress (). getCountry (), deepCopy.getAddress (). getCountry ()); }

5.2. Klonabelt grensesnitt

Den andre implementeringen er basert på klonmetoden arvet fra Gjenstand. Det er beskyttet, men vi må overstyre det som offentlig.

Vi legger også til et markørgrensesnitt, Klonbar, til klassene for å indikere at klassene faktisk er klonbare.

La oss legge til klone () metoden til Adresse klasse:

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

Og la oss nå implementere klone () for Bruker klasse:

@ Override public Object clone () {Brukerbruker = null; prøv {user = (User) super.clone (); } fange (CloneNotSupportedException e) {user = new User (this.getFirstName (), this.getLastName (), this.getAddress ()); } user.address = (Adresse) this.address.clone (); retur bruker; }

Merk at super.clone () anrop returnerer en grunne kopi av et objekt, men vi setter dype kopier av foranderlige felt manuelt, slik at resultatet er riktig:

@Test offentlig ugyldig nårModifyingOriginalObject_thenCloneCopyShouldNotChange () {Adresse adresse = ny adresse ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); User deepCopy = (User) pm.clone (); address.setCountry ("Storbritannia"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6. Eksterne biblioteker

Ovennevnte eksempler ser enkle ut, men noen ganger gjelder de ikke som en løsning når vi ikke kan legge til en ekstra konstruktør eller overstyre klonmetoden.

Dette kan skje når vi ikke eier koden, eller når objektgrafen er så komplisert at vi ikke ville fullføre prosjektet i tide hvis vi fokuserte på å skrive flere konstruktører eller implementere klone metode på alle klasser i objektgrafen.

Hva da? I dette tilfellet kan vi bruke et eksternt bibliotek. For å oppnå en dyp kopi, vi kan serieisere et objekt og deretter deserialisere det til et nytt objekt.

La oss se på noen få eksempler.

6.1. Apache Commons Lang

Apache Commons Lang har SerializationUtils # klon, som utfører en dyp kopi når alle klassene i objektgrafen implementerer Serialiserbar grensesnitt.

Hvis metoden møter en klasse som ikke kan serieiseres, vil den mislykkes og kaste en ukontrollert SerializationException.

På grunn av det må vi legge til Serialiserbar grensesnitt til klassene våre:

@Test offentlig ugyldig nårModifyingOriginalObject_thenCommonsCloneShouldNotChange () {Adresse adresse = ny adresse ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); User deepCopy = (User) SerializationUtils.clone (pm); address.setCountry ("Storbritannia"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6.2. JSON Serialization With Gson

Den andre måten å serialisere er å bruke JSON-serialisering. Gson er et bibliotek som brukes til å konvertere objekter til JSON og omvendt.

I motsetning til Apache Commons Lang, GSON trenger ikke Serialiserbar grensesnitt for å gjøre konverteringene.

La oss se raskt på et eksempel:

@Test offentlig ugyldig nårModifyingOriginalObject_thenGsonCloneShouldNotChange () {Address address = new Address ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); Gson gson = ny Gson (); User deepCopy = gson.fromJson (gson.toJson (pm), User.class); address.setCountry ("Storbritannia"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

6.3. JSON Serialization With Jackson

Jackson er et annet bibliotek som støtter JSON-serialisering. Denne implementeringen vil være veldig lik den som bruker Gson, men vi må legge til standardkonstruktøren i klassene våre.

La oss se et eksempel:

@Test offentlig ugyldig nårModifyingOriginalObject_thenJacksonCopyShouldNotChange () kaster IOException {Address address = new Address ("Downing St 10", "London", "England"); Bruker pm = ny bruker ("Prime", "Minister", adresse); ObjectMapper objectMapper = ny ObjectMapper (); User deepCopy = objectMapper .readValue (objectMapper.writeValueAsString (pm), User.class); address.setCountry ("Storbritannia"); assertThat (deepCopy.getAddress (). getCountry ()) .isNotEqualTo (pm.getAddress (). getCountry ()); }

7. Konklusjon

Hvilken implementering skal vi bruke når vi lager en dyp kopi? Den endelige avgjørelsen vil ofte avhenge av klassene vi kopierer og om vi eier klassene i objektgrafen.

Som alltid kan de komplette kodeeksemplene for denne opplæringen finnes på GitHub.