Comment créer une copie complète d’un objet en Java

Comment faire une copie profonde d'un objet en Java

1. introduction

Lorsque nous voulons copier un objet en Java, nous devons prendre en compte deux possibilités: une copie superficielle et une copie profonde.

La copie superficielle est l’approche lorsque nous ne copions que les valeurs de champ et que, par conséquent, la copie peut dépendre de l’objet original. Dans l'approche de copie profonde, nous nous assurons que tous les objets de l'arborescence sont copiés en profondeur, de sorte que la copie ne dépend pas d'un objet existant antérieur qui pourrait changer.

Dans cet article, nous comparerons ces deux approches et découvrirons quatre méthodes pour implémenter la copie complète.

2. Maven Setup

Nous allons utiliser 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 à nospom.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 deGson,Jackson etApache Commons Lang sont disponibles sur Maven Central.

3. Modèle

Pour comparer différentes méthodes de copie d'objets Java, nous avons 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 laquellewe only copy values of fields 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 quethey’re different objects, but the problem is that when we change any of the original address' properties, this will also affect the shallowCopy‘s address.

Cela ne nous dérangerait pas siAddress é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 en profondeur est une alternative qui résout ce problème. Son avantage est qu'au moinseach mutable object in the object graph is recursively copied.

É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 allons montrer plusieurs implémentations de copie profonde et démontrer cet avantage.

5.1. Copier le constructeur

La première implémentation que nous allons implémenter 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 nouveauxStrings dans notre constructeur de copie carString 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 deObject. Il est protégé, mais nous devons le remplacer parpublic.

Nous allons également ajouter une interface de marqueur,Cloneable, aux classes pour indiquer que les classes sont réellement clonables.

Ajoutons la méthodeclone() à la classeAddress:

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

Et maintenant, implémentonsclone() pour la classeUser:

@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'appelsuper.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 simples, mais parfois ils ne s’appliquent pas en tant que solutionwhen we can’t add an additional constructor or override the clone method.

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éthodeclone sur toutes les classes de le graphe d'objets.

Quoi alors? Dans ce cas, nous pouvons utiliser une bibliothèque externe. Pour obtenir une copie complète,we can serialize an object and then deserialize it to a new object.

Regardons quelques exemples.

6.1. Apache Commons Lang

Apache Commons Lang aSerializationUtils#clone, qui effectue une copie complète lorsque toutes les classes du graphe d'objets implémentent l'interfaceSerializable.

Si la méthode rencontre une classe qui n’est pas sérialisable, elle échouera et lancera unSerializationException non coché.

Pour cette raison, nous devons ajouter l'interfaceSerializable à nos 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. Sérialisation JSON avec Gson

L'autre façon de procéder à la sérialisation consiste à utiliser la sérialisation JSON. Gson est une bibliothèque utilisée pour convertir des objets en JSON et vice versa.

Contrairement à Apache Commons Lang,GSON does not need the Serializable interface to make the conversions.

Jetons un coup d'œil à un exemple:

@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, maiswe need to add the default constructor to our 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 devrions-nous utiliser pour réaliser une copie en profondeur? 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 pour ce didacticiel peuvent être trouvésover on Github.