Comment faire une copie complète d'un objet en Java

1. Introduction

Lorsque nous voulons copier un objet en Java, il y a deux possibilités que nous devons considérer: une copie superficielle et une copie profonde.

La copie superficielle est l'approche lorsque nous ne copions que les valeurs de champ et que la copie peut donc dépendre de l'objet d'origine. Dans l'approche de copie profonde, nous nous assurons que tous les objets de l'arborescence sont profondément copiés, de sorte que la copie ne dépend d'aucun objet existant antérieur qui pourrait jamais changer.

Dans cet article, nous allons comparer ces deux approches et apprendre quatre méthodes pour implémenter la copie profonde.

2. Configuration de Maven

Nous utiliserons trois dépendances Maven - Gson, Jackson et Apache Commons Lang - pour tester différentes façons d'effectuer une copie complète.

Ajoutons ces dépendances à notre 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 

Les dernières versions de Gson, Jackson et Apache Commons Lang sont disponibles sur Maven Central.

3. Modèle

Pour comparer différentes méthodes de copie d'objets Java, nous aurons besoin de deux classes sur lesquelles travailler:

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. Copie superficielle

Une copie superficielle est une copie dans laquelle nous ne copions que les valeurs des champs d'un objet à un autre:

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

Dans ce cas, pm! = ShallowCopy , ce qui signifie qu'il s'agit d'objets différents, mais le problème est que lorsque nous modifions l'une des propriétés de l' adresse d' origine , cela affectera également l' adresse de shallowCopy .

Cela ne nous dérangerait pas si Address était immuable, mais ce n'est pas le cas:

@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. Copie profonde

Une copie complète est une alternative qui résout ce problème. Son avantage est qu'au moins chaque objet mutable dans le graphe d'objets est copié récursivement .

Étant donné que la copie ne dépend d'aucun objet mutable créé précédemment, elle ne sera pas modifiée par accident comme nous l'avons vu avec la copie superficielle.

Dans les sections suivantes, nous montrerons plusieurs implémentations de copie profonde et démontrerons cet avantage.

5.1. Copier le constructeur

La première implémentation que nous implémenterons est basée sur des constructeurs de copie:

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

Dans l'implémentation ci-dessus de la copie profonde, nous n'avons pas créé de nouvelles chaînes dans notre constructeur de copie car String est une classe immuable.

En conséquence, ils ne peuvent pas être modifiés par accident. Voyons si cela fonctionne:

@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. Interface clonable

La deuxième implémentation est basée sur la méthode de clonage héritée d' Object . Il est protégé, mais nous devons le remplacer en tant que public .

Nous ajouterons également une interface de marqueur, Clonable, aux classes pour indiquer que les classes sont réellement clonables.

Ajoutons la méthode clone () à la classe Address :

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

Et maintenant, implémentons clone () pour 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; }

Notez que l' appel super.clone () renvoie une copie superficielle d'un objet, mais nous définissons manuellement des copies complètes des champs mutables, donc le résultat est correct:

@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. Bibliothèques externes

Les exemples ci-dessus semblent faciles, mais parfois ils ne s'appliquent pas comme solution lorsque nous ne pouvons pas ajouter un constructeur supplémentaire ou remplacer la méthode de clonage .

Cela peut se produire lorsque nous ne possédons pas le code, ou lorsque le graphe d'objets est si compliqué que nous ne terminerions pas notre projet à temps si nous nous concentrions sur l'écriture de constructeurs supplémentaires ou l'implémentation de la méthode clone sur toutes les classes du graphe d'objets.

Et alors? Dans ce cas, nous pouvons utiliser une bibliothèque externe. Pour obtenir une copie complète, nous pouvons sérialiser un objet, puis le désérialiser en un nouvel objet .

Regardons quelques exemples.

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. Sérialisation JSON avec Jackson

Jackson est une autre bibliothèque qui prend en charge la sérialisation JSON. Cette implémentation sera très similaire à celle utilisant Gson, mais nous devons ajouter le constructeur par défaut à nos classes .

Voyons un exemple:

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

Quelle implémentation devons-nous utiliser lors de la création d'une copie complète? La décision finale dépendra souvent des classes que nous copierons et si nous possédons les classes dans le graphe d'objets.

Comme toujours, les exemples de code complets de ce didacticiel sont disponibles sur GitHub.