Come creare una copia profonda di un oggetto in Java

1. Introduzione

Quando vogliamo copiare un oggetto in Java, ci sono due possibilità che dobbiamo considerare: una copia superficiale e una copia profonda.

La copia superficiale è l'approccio quando copiamo solo i valori dei campi e quindi la copia potrebbe dipendere dall'oggetto originale. Nell'approccio della copia profonda, ci assicuriamo che tutti gli oggetti nell'albero siano copiati in profondità, quindi la copia non dipende da alcun oggetto esistente precedente che potrebbe mai cambiare.

In questo articolo confronteremo questi due approcci e impareremo quattro metodi per implementare la copia completa.

2. Installazione di Maven

Useremo tre dipendenze Maven - Gson, Jackson e Apache Commons Lang - per testare diversi modi di eseguire una copia completa.

Aggiungiamo queste dipendenze al nostro 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 

Le ultime versioni di Gson, Jackson e Apache Commons Lang sono disponibili su Maven Central.

3. Modello

Per confrontare diversi metodi per copiare oggetti Java, avremo bisogno di due classi su cui lavorare:

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. Copia superficiale

Una copia superficiale è quella in cui copiamo solo i valori dei campi da un oggetto a un altro:

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

In questo caso pm! = ShallowCopy , il che significa che sono diversi oggetti, ma il problema è che quando si cambia una delle originali di indirizzo proprietà, questo influirà anche l' shallowCopy 'indirizzo di s .

Non ci preoccuperemmo se Address fosse immutabile, ma non lo è:

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

Una copia completa è un'alternativa che risolve questo problema. Il suo vantaggio è che almeno ogni oggetto mutabile nell'oggetto grafico viene copiato ricorsivamente .

Poiché la copia non dipende da alcun oggetto modificabile creato in precedenza, non verrà modificata accidentalmente come abbiamo visto con la copia superficiale.

Nelle sezioni seguenti, mostreremo diverse implementazioni di copia completa e dimostreremo questo vantaggio.

5.1. Copia costruttore

La prima implementazione che implementeremo si basa sui costruttori di copia:

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

Nell'implementazione precedente della copia completa, non abbiamo creato nuove stringhe nel nostro costruttore di copie perché String è una classe immutabile.

Di conseguenza, non possono essere modificati accidentalmente. Vediamo se funziona:

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

La seconda implementazione si basa sul metodo clone ereditato da Object . È protetto, ma dobbiamo sovrascriverlo come pubblico .

Aggiungeremo anche un'interfaccia marker, Cloneable, alle classi per indicare che le classi sono effettivamente clonabili.

Aggiungiamo il metodo clone () alla classe Address :

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

E ora implementiamo clone () per la classe 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; }

Nota che la chiamata super.clone () restituisce una copia superficiale di un oggetto, ma impostiamo manualmente copie profonde dei campi modificabili, quindi il risultato è corretto:

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

Gli esempi precedenti sembrano facili, ma a volte non si applicano come soluzione quando non è possibile aggiungere un ulteriore costruttore o sovrascrivere il metodo clone .

Ciò potrebbe accadere quando non possediamo il codice o quando l'oggetto grafico è così complicato che non finiremmo il nostro progetto in tempo se ci concentrassimo sulla scrittura di costruttori aggiuntivi o sull'implementazione del metodo clone su tutte le classi nell'oggetto grafico.

Cosa poi? In questo caso, possiamo utilizzare una libreria esterna. Per ottenere una copia completa, possiamo serializzare un oggetto e quindi deserializzarlo in un nuovo oggetto .

Diamo un'occhiata ad alcuni esempi.

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. Serializzazione JSON con Jackson

Jackson è un'altra libreria che supporta la serializzazione JSON. Questa implementazione sarà molto simile a quella che utilizza Gson, ma dobbiamo aggiungere il costruttore predefinito alle nostre classi .

Vediamo un esempio:

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

Quale implementazione dovremmo usare quando creiamo una copia completa? La decisione finale dipenderà spesso dalle classi che copieremo e se possediamo le classi nell'oggetto grafico.

Come sempre, gli esempi di codice completi per questo tutorial sono disponibili su GitHub.