So erstellen Sie eine tiefe Kopie eines Objekts in Java

1. Einleitung

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, wenn nur Feldwerte kopiert werden und die Kopie daher möglicherweise vom ursprünglichen Objekt abhängig ist. 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 unserer pom.xml hinzu :

 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 von Gson, Jackson und Apache Commons Lang finden Sie auf 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, bei der nur Werte von Feldern von einem Objekt in ein anderes kopiert werden :

@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 Fall ist pm! = Flachwertig , was bedeutet, dass es sich um verschiedene Objekte handelt. Das Problem besteht jedoch darin, dass sich das Ändern der Eigenschaften der ursprünglichen Adresse auch auf die Adresse von flachCopy auswirkt .

Wir würden uns nicht darum kümmern, wenn die Adresse 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 mindestens jedes veränderbare Objekt im Objektgraphen rekursiv kopiert wird .

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 neuen Strings erstellt, da String eine unveränderliche Klasse ist.

Sie können daher 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 von Object geerbten Klonmethode . Es ist geschützt, aber wir müssen es als öffentlich überschreiben .

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

Lassen Sie sich die Add - Klon () Methode , um die Adressklasse:

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

Und jetzt implementieren wir clone () für die User- Klasse:

@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 von super.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, aber manchmal gelten sie nicht als Lösung, wenn wir keinen zusätzlichen Konstruktor hinzufügen oder die Klonmethode überschreiben können .

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 die Klonmethode für alle Klassen im Objektdiagramm zu implementieren .

Was dann? In diesem Fall können wir eine externe Bibliothek verwenden. Um eine tiefe Kopie zu erhalten, können wir ein Objekt serialisieren und dann in ein neues Objekt deserialisieren .

Schauen wir uns einige Beispiele an.

6.1. Apache Commons Lang

Apache Commons Lang verfügt über den SerializationUtils # -Klon, der eine Tiefenkopie ausführt, wenn alle Klassen im Objektdiagramm die Serializable- Schnittstelle implementieren .

Wenn die Methode auf eine Klasse trifft, die nicht serialisierbar ist, schlägt sie fehl und löst eine nicht aktivierte SerializationException aus .

Aus diesem Grund müssen wir unseren Klassen die serialisierbare Schnittstelle hinzufü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 zur Serialisierung ist die Verwendung der JSON-Serialisierung. Gson ist eine Bibliothek, mit der Objekte in JSON konvertiert werden und umgekehrt.

Im Gegensatz zu Apache Commons Lang benötigt GSON für die Konvertierungen keine serialisierbare Schnittstelle .

Schauen wir uns ein Beispiel kurz 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, aber wir müssen unseren Klassen den Standardkonstruktor hinzufügen .

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 verwenden, wenn wir eine tiefe Kopie erstellen? Die endgültige Entscheidung hängt oft 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 auf GitHub.