Hur man gör en djup kopia av ett objekt i Java

1. Introduktion

När vi vill kopiera ett objekt i Java finns det två möjligheter som vi måste tänka på - en grund kopia och en djup kopia.

Den grunda kopian är tillvägagångssättet när vi bara kopierar fältvärden och därför kan kopian vara beroende av det ursprungliga objektet. I djupkopieringsmetoden ser vi till att alla objekt i trädet kopieras djupt, så kopian är inte beroende av något tidigare existerande objekt som någonsin kan förändras.

I den här artikeln jämför vi dessa två tillvägagångssätt och lär oss fyra metoder för att implementera den djupa kopian.

2. Inställning av Maven

Vi använder tre Maven-beroenden - Gson, Jackson och Apache Commons Lang - för att testa olika sätt att utföra en djup kopia.

Låt oss lägga till dessa beroenden i 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 senaste versionerna av Gson, Jackson och Apache Commons Lang finns på Maven Central.

3. Modell

För att jämföra olika metoder för att kopiera Java-objekt behöver vi två klasser att arbeta med:

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. Grunt kopia

En grunt kopia är en där vi bara kopierar värden för fält från ett objekt till ett annat:

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

I det här fallet pm! = Ytlig , vilket innebär att de är olika objekt, men problemet är att när vi ändrar något av de ursprungliga adress egenskaper, kommer detta också att påverka ytlig : s adress .

Vi skulle inte bry oss om det om Adress var oföränderlig, men det är inte:

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

En djup kopia är ett alternativ som löser detta problem. Dess fördel är att åtminstone varje föränderligt objekt i objektgrafen kopieras rekursivt .

Eftersom kopian inte är beroende av något föränderligt objekt som skapades tidigare kommer det inte att ändras av misstag som vi såg med den grunda kopian.

I följande avsnitt visar vi flera implementeringar av djupa kopior och visar denna fördel.

5.1. Copy Constructor

Den första implementeringen vi implementerar är baserad på kopiekonstruktörer:

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

I ovanstående implementering av den djupa kopian har vi inte skapat nya strängar i vår kopiekonstruktör eftersom String är en oföränderlig klass.

Som ett resultat kan de inte ändras av misstag. Låt oss se om detta fungerar:

@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. Klonabelt gränssnitt

Den andra implementeringen baseras på den klonmetod som ärvs från Object . Det är skyddat, men vi måste åsidosätta det som offentligt .

Vi lägger också till ett markörgränssnitt, Cloneable, i klasserna för att indikera att klasserna faktiskt är klonbara.

Låt oss lägga till klonmetoden () i adressklassen :

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

Och nu ska vi genomföra clone () för Användarklass:

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

Observera att super.clone () -anropet returnerar en grunt kopia av ett objekt, men vi ställer in djupa kopior av mutabla fält manuellt så att resultatet blir korrekt:

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

Ovanstående exempel ser enkla ut, men ibland gäller de inte som en lösning när vi inte kan lägga till ytterligare en konstruktör eller åsidosätta klonmetoden .

Detta kan hända när vi inte äger koden, eller när objektgrafen är så komplicerad att vi inte skulle avsluta vårt projekt i tid om vi fokuserade på att skriva ytterligare konstruktörer eller implementera klonmetoden på alla klasser i objektgrafen.

Vad händer då? I det här fallet kan vi använda ett externt bibliotek. För att uppnå en djup kopia kan vi serieera ett objekt och deserialisera det till ett nytt objekt .

Låt oss titta på några exempel.

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 Serialization With Jackson

Jackson är ett annat bibliotek som stöder JSON-serialisering. Denna implementering kommer att likna den som använder Gson, men vi måste lägga till standardkonstruktören i våra klasser .

Låt oss se ett exempel:

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

Vilket implementering ska vi använda när vi gör en djup kopia? Det slutgiltiga beslutet beror ofta på de klasser vi kopierar och om vi äger klasserna i objektgrafen.

Som alltid kan de fullständiga kodproverna för den här självstudien hittas på GitHub.