So erstellen Sie eine tiefe Kopie eines Objekts in Java

So erstellen Sie eine vertiefte Kopie eines Objekts in Java

1. Einführung

Wenn wir ein Objekt in Java kopieren möchten, müssen wir zwei Möglichkeiten in Betracht ziehen - eine flache Kopie und eine tiefe Kopie.

Die flache Kopie ist der Ansatz, bei dem nur Feldwerte kopiert werden und die Kopie daher möglicherweise vom ursprünglichen Objekt abhängt. Beim Deep Copy-Ansatz stellen wir sicher, dass alle Objekte im Baum tief kopiert werden, damit die Kopie nicht von einem früheren vorhandenen Objekt abhängig ist, das sich jemals ändern könnte.

In diesem Artikel werden wir diese beiden Ansätze vergleichen und vier Methoden zum Implementieren der Deep Copy lernen.

2. Maven Setup

Wir werden drei Maven-Abhängigkeiten verwenden - Gson, Jackson und Apache Commons Lang -, um verschiedene Methoden zum Ausführen einer tiefen Kopie zu testen.

Fügen wir diese Abhängigkeiten zu unserenpom.xmlhinzu:


    com.google.code.gson
    gson
    2.8.2


    commons-lang
    commons-lang
    2.6


    com.fasterxml.jackson.core
    jackson-databind
    2.9.3

Die neuesten Versionen vonGson,Jackson undApache Commons Lang finden Sie in Maven Central.

3. Modell

Um verschiedene Methoden zum Kopieren von Java-Objekten zu vergleichen, benötigen wir zwei Klassen:

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. Flache Kopie

Eine flache Kopie ist eine Kopie, in derwe only copy values of fieldsvon einem Objekt zum anderen wechselt:

@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 diesem Fallpm != shallowCopy, was bedeutet, dassthey’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.

Wir würden uns nicht darum kümmern, wennAddress unveränderlich wäre, aber es ist nicht:

@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. Deep Copy

Eine tiefe Kopie ist eine Alternative, die dieses Problem löst. Sein Vorteil ist, dass mindestenseach mutable object in the object graph is recursively copied.

Da die Kopie nicht von einem zuvor erstellten veränderlichen Objekt abhängig ist, wird sie nicht versehentlich geändert, wie wir es bei der flachen Kopie gesehen haben.

In den folgenden Abschnitten werden einige Deep-Copy-Implementierungen gezeigt und dieser Vorteil demonstriert.

5.1. Konstruktor kopieren

Die erste Implementierung, die wir implementieren, basiert auf Kopierkonstruktoren:

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

In der obigen Implementierung der Deep Copy haben wir in unserem Kopierkonstruktor keine neuenStrings erstellt, daString eine unveränderliche Klasse ist.

Daher können sie nicht versehentlich geändert werden. Mal sehen, ob das funktioniert:

@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. Klonbare Schnittstelle

Die zweite Implementierung basiert auf der vonObject geerbten Klonmethode. Es ist geschützt, aber wir müssen es alspublic überschreiben.

Wir werden den Klassen auch eine MarkierungsschnittstelleCloneable, hinzufügen, um anzuzeigen, dass die Klassen tatsächlich klonbar sind.

Fügen wir der MethodeAddressdie Methodeclone()hinzu:

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

Und jetzt implementieren wirclone() für die KlasseUser:

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

Beachten Sie, dass der Aufruf vonsuper.clone()eine flache Kopie eines Objekts zurückgibt, wir jedoch tiefe Kopien von veränderlichen Feldern manuell festlegen, sodass das Ergebnis korrekt ist:

@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. Externe Bibliotheken

Die obigen Beispiele sehen einfach aus, gelten jedoch manchmal nicht als Lösungwhen we can’t add an additional constructor or override the clone method.

Dies kann passieren, wenn wir den Code nicht besitzen oder wenn das Objektdiagramm so kompliziert ist, dass wir unser Projekt nicht rechtzeitig beenden würden, wenn wir uns darauf konzentrieren würden, zusätzliche Konstruktoren zu schreiben oder dieclone-Methode für alle Klassen in zu implementieren das Objektdiagramm.

Was dann? In diesem Fall können wir eine externe Bibliothek verwenden. Um eine tiefe Kopie zu erhalten,we can serialize an object and then deserialize it to a new object.

Schauen wir uns einige Beispiele an.

6.1. Apache Commons Lang

Apache Commons Lang hatSerializationUtils#clone,, das eine tiefe Kopie ausführt, wenn alle Klassen im Objektdiagramm dieSerializable-Schnittstelle implementieren.

Wenn die Methode auf eine Klasse trifft, die nicht serialisierbar ist, schlägt sie fehl und gibt ein ungeprüftesSerializationException aus.

Aus diesem Grund müssen wir unseren Klassen die SchnittstelleSerializablehinzufügen:

@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-Serialisierung mit Gson

Die andere Möglichkeit zum Serialisieren besteht in der Verwendung der JSON-Serialisierung. Gson ist eine Bibliothek, mit der Objekte in JSON konvertiert werden und umgekehrt.

Im Gegensatz zu Apache Commons Lang istGSON does not need the Serializable interface to make the conversions.

Schauen wir uns ein kurzes Beispiel an:

@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-Serialisierung mit Jackson

Jackson ist eine weitere Bibliothek, die die JSON-Serialisierung unterstützt. Diese Implementierung wird der mit Gson sehr ähnlich sein, jedochwe need to add the default constructor to our classes.

Sehen wir uns ein Beispiel an:

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

Welche Implementierung sollten wir beim Erstellen einer tiefen Kopie verwenden? Die endgültige Entscheidung hängt häufig von den Klassen ab, die wir kopieren, und davon, ob wir die Klassen im Objektdiagramm besitzen.

Wie immer finden Sie die vollständigen Codebeispiele für dieses Tutorial inover on Github.