Wie macht man eine tiefe Kopie eines Objekts?

301

Es ist etwas schwierig, eine Deep Object Copy-Funktion zu implementieren. Welche Schritte unternehmen Sie, um sicherzustellen, dass das ursprüngliche und das geklonte Objekt keinen Bezug haben?

Andrei Savu
quelle
4
Kryo verfügt über eine integrierte Unterstützung für das Kopieren / Klonen . Dies ist direktes Kopieren von Objekt zu Objekt, nicht Objekt-> Bytes-> Objekt.
NateS
1
Hier ist eine verwandte Frage, die später gestellt wurde: Deep Clone Utility-Empfehlung
Brad Cupit
Die Verwendung der Klonbibliothek hat mir den Tag gerettet! github.com/kostaskougios/cloning
gaurav

Antworten:

168

Ein sicherer Weg besteht darin, das Objekt zu serialisieren und dann zu deserialisieren. Dies stellt sicher, dass alles eine brandneue Referenz ist.

Hier ist ein Artikel darüber, wie dies effizient durchgeführt werden kann.

Vorsichtsmaßnahmen: Klassen können die Serialisierung überschreiben, sodass keine neuen Instanzen erstellt werden, z. B. für Singletons. Auch dies funktioniert natürlich nicht, wenn Ihre Klassen nicht serialisierbar sind.

Jason Cohen
quelle
6
Beachten Sie, dass die im Artikel bereitgestellte FastByteArrayOutputStream-Implementierung effizienter sein kann. Es wird eine Erweiterung im ArrayList-Stil verwendet, wenn der Puffer voll ist. Es ist jedoch besser, einen Erweiterungsansatz im LinkedList-Stil zu verwenden. Anstatt einen neuen 2x-Puffer zu erstellen und den aktuellen Puffer zu speichern, pflegen Sie eine verknüpfte Liste von Puffern und fügen Sie einen neuen hinzu, wenn der aktuelle Puffer voll ist. Wenn Sie die Anforderung erhalten, mehr Daten zu schreiben, als in Ihre Standardpuffergröße passen, erstellen Sie einen Pufferknoten, der genau so groß ist wie die Anforderung. Die Knoten müssen nicht gleich groß sein.
Brian Harris
Ein guter Artikel, der die tiefe Kopie durch Serialisierung erklärt: javaworld.com/article/2077578/learn-java/…
Ad Infinitum
Die verknüpfte @ BrianHarris-Liste ist nicht effizienter als ein dynamisches Array. Das Einfügen von Elementen in ein dynamisches Array wird mit konstanter Komplexität abgeschrieben, während das Einfügen in eine verknüpfte Liste eine lineare Komplexität darstellt
Norill Tempest,
Wie viel serialisieren und deserialisieren Sie langsamer als beim Kopierkonstruktor?
Woland
75

Einige Leute haben die Verwendung oder Überschreibung erwähnt Object.clone(). Tu es nicht. Object.clone()hat einige große Probleme, und von seiner Verwendung wird in den meisten Fällen abgeraten. Eine vollständige Antwort finden Sie unter Punkt 11 aus " Effective Java " von Joshua Bloch. Ich glaube, Sie können sicher Object.clone()auf Arrays vom primitiven Typ verwenden, aber abgesehen davon müssen Sie vorsichtig sein, wenn Sie den Klon richtig verwenden und überschreiben.

Die Schemata, die auf Serialisierung (XML oder auf andere Weise) beruhen, sind klobig.

Hier gibt es keine einfache Antwort. Wenn Sie ein Objekt tief kopieren möchten, müssen Sie das Objektdiagramm durchlaufen und jedes untergeordnete Objekt explizit über den Kopierkonstruktor des Objekts oder eine statische Factory-Methode kopieren, die wiederum das untergeordnete Objekt tief kopiert. Unveränderliche (z. B. String) müssen nicht kopiert werden. Nebenbei sollten Sie aus diesem Grund die Unveränderlichkeit bevorzugen.

Julien Chastang
quelle
58

Sie können eine tiefe Kopie mit Serialisierung erstellen, ohne Dateien zu erstellen.

Ihr Objekt, das Sie tief kopieren möchten, muss implement serializable . Wenn die Klasse nicht endgültig ist oder nicht geändert werden kann, erweitern Sie die Klasse und implementieren Sie serialisierbar.

Konvertieren Sie Ihre Klasse in einen Stream von Bytes:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.flush();
oos.close();
bos.close();
byte[] byteData = bos.toByteArray();

Stellen Sie Ihre Klasse aus einem Bytestrom wieder her:

ByteArrayInputStream bais = new ByteArrayInputStream(byteData);
(Object) object = (Object) new ObjectInputStream(bais).readObject();
Thargor
quelle
4
Wenn die Klasse endgültig ist, wie würden Sie sie erweitern?
Kumar Manish
1
@ KumarManish-Klasse MyContainer implementiert Serializable {MyFinalClass-Instanz; ...}
Matteo T.
Ich finde das eine tolle Antwort. Klon ist ein Chaos
blackbird014
@MatteoT. Wie wird die nicht serialisierbare Klasseneigenschaft serialisiert, instancein diesem Fall nicht serialisierbar ?
Farid
40

Sie können org.apache.commons.lang3.SerializationUtils.clone(T)in Apache Commons Lang einen auf Serialisierung basierenden Deep Clone erstellen, aber seien Sie vorsichtig - die Leistung ist miserabel.

Im Allgemeinen empfiehlt es sich, für jede Klasse eines Objekts im zu klonenden Objektdiagramm eigene Klonmethoden zu schreiben.

user8690
quelle
Es ist auch verfügbar inorg.apache.commons.lang.SerializationUtils
Pino
25

Eine Möglichkeit, Deep Copy zu implementieren, besteht darin, jeder zugeordneten Klasse Kopierkonstruktoren hinzuzufügen. Ein Kopierkonstruktor verwendet eine Instanz von 'this' als einzelnes Argument und kopiert alle Werte daraus. Ziemlich viel Arbeit, aber ziemlich unkompliziert und sicher.

BEARBEITEN: Beachten Sie, dass Sie zum Lesen von Feldern keine Zugriffsmethoden verwenden müssen. Sie können direkt auf alle Felder zugreifen, da die Quellinstanz immer vom gleichen Typ ist wie die Instanz mit dem Kopierkonstruktor. Offensichtlich, könnte aber übersehen werden.

Beispiel:

public class Order {

    private long number;

    public Order() {
    }

    /**
     * Copy constructor
     */
    public Order(Order source) {
        number = source.number;
    }
}


public class Customer {

    private String name;
    private List<Order> orders = new ArrayList<Order>();

    public Customer() {
    }

    /**
     * Copy constructor
     */
    public Customer(Customer source) {
        name = source.name;
        for (Order sourceOrder : source.orders) {
            orders.add(new Order(sourceOrder));
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Bearbeiten: Beachten Sie, dass Sie bei Verwendung von Kopierkonstruktoren den Laufzeittyp des zu kopierenden Objekts kennen müssen. Mit dem obigen Ansatz können Sie eine gemischte Liste nicht einfach kopieren (möglicherweise können Sie dies mit einem Reflexionscode tun).

Adriaan Koster
quelle
1
Nur interessiert für den Fall, dass das, was Sie kopieren, eine Unterklasse ist, aber vom übergeordneten Element referenziert wird. Ist es möglich, den Kopierkonstruktor zu überschreiben?
Pork 'n' Bunny
Warum bezieht sich Ihre Elternklasse auf ihre Unterklasse? Kannst du ein Beispiel geben?
Adriaan Koster
1
öffentliches Auto erweitert Fahrzeug und bezieht sich dann auf das Auto als Fahrzeug. originaList = new ArrayList <Fahrzeug>; copyList = new ArrayList <Fahrzeug>; originalList.add (neues Auto ()); für (Fahrzeug Fahrzeug: Fahrzeugliste) {copyList.add (neues Fahrzeug (Fahrzeug)); }
Pork 'n' Bunny
@AdriaanKoster: Wenn die ursprüngliche Liste ein enthält Toyota, wird Ihr Code ein Carin die Zielliste einfügen . Für ein ordnungsgemäßes Klonen muss die Klasse im Allgemeinen eine virtuelle Factory-Methode bereitstellen, deren Vertrag besagt, dass sie ein neues Objekt ihrer eigenen Klasse zurückgibt. Der Kopierkonstruktor selbst sollte protectedsicherstellen, dass er nur zum Erstellen von Objekten verwendet wird, deren genauer Typ mit dem des zu kopierenden Objekts übereinstimmt.
Supercat
Wenn ich Ihren Vorschlag richtig verstehe, würde die Factory-Methode den Konstruktor für private Kopien aufrufen? Wie würde der Kopierkonstruktor einer Unterklasse sicherstellen, dass die Felder der Oberklasse initialisiert werden? Kannst du ein Beispiel geben?
Adriaan Koster
20

Sie können eine Bibliothek verwenden , die über eine einfache API verfügt und ein relativ schnelles Klonen mit Reflektion durchführt (sollte schneller sein als Serialisierungsmethoden).

Cloner cloner = new Cloner();

MyClass clone = cloner.deepClone(o);
// clone is a deep-clone of o
CorayThan
quelle
19

Apache Commons bietet eine schnelle Möglichkeit, ein Objekt tief zu klonen.

My_Object object2= org.apache.commons.lang.SerializationUtils.clone(object1);
TheByeByeMan
quelle
1
Dies funktioniert nur für das Objekt, das Serializable implementiert, und auch für alle darin enthaltenen Felder, die Serializable implementieren.
Wonhee
11

XStream ist in solchen Fällen sehr nützlich. Hier ist ein einfacher Code zum Klonen

private static final XStream XSTREAM = new XStream();
...

Object newObject = XSTREAM.fromXML(XSTREAM.toXML(obj));
Sankara
quelle
1
Nein, Sie brauchen nicht den Aufwand für die XML-Verknüpfung des Objekts.
Egelev
@egeleve Du merkst doch, dass du auf einen Kommentar von '08 antwortest, oder? Ich benutze kein Java mehr und es gibt jetzt wahrscheinlich bessere Tools. Zu dieser Zeit schien es jedoch ein guter Hack zu sein, in ein anderes Format zu serialisieren und dann zurück zu serialisieren - es war definitiv ineffizient.
Sankara
10

Für Spring Framework- Benutzer. Klasse verwenden org.springframework.util.SerializationUtils:

@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T object) {
     return (T) SerializationUtils.deserialize(SerializationUtils.serialize(object));
}
Igor Rybak
quelle
9

Für komplizierte Objekte und wenn die Leistung nicht signifikant ist, verwende ich eine JSON-Bibliothek wie Gson um das Objekt in JSON-Text zu serialisieren, und deserialisiere dann den Text, um ein neues Objekt zu erhalten.

gson, das auf Reflexion basiert, funktioniert in den meisten Fällen, außer dass transientFelder nicht kopiert werden und Objekte mit Zirkelverweis mit Ursache StackOverflowError.

public static <T> T copy(T anObject, Class<T> classInfo) {
    Gson gson = new GsonBuilder().create();
    String text = gson.toJson(anObject);
    T newObject = gson.fromJson(text, classInfo);
    return newObject;
}
public static void main(String[] args) {
    String originalObject = "hello";
    String copiedObject = copy(originalObject, String.class);
}
tiboo
quelle
3
Bitte halten Sie sich für sich und für uns an die Java-Namenskonventionen.
Patrick Bergner
8

Verwenden Sie XStream ( http://x-stream.github.io/ ). Sie können sogar steuern, welche Eigenschaften Sie durch Anmerkungen ignorieren oder den Eigenschaftsnamen explizit für die XStream-Klasse angeben können. Darüber hinaus müssen Sie keine klonbare Schnittstelle implementieren.

Adisesha
quelle
7

Tiefes Kopieren kann nur mit Zustimmung jeder Klasse durchgeführt werden. Wenn Sie die Kontrolle über die Klassenhierarchie haben, können Sie die klonbare Schnittstelle und die Clone-Methode implementieren. Andernfalls ist eine sichere Kopie nicht sicher durchzuführen, da das Objekt möglicherweise auch Nicht-Datenressourcen (z. B. Datenbankverbindungen) gemeinsam nutzt. Im Allgemeinen wird jedoch tiefes Kopieren in der Java-Umgebung als schlechte Praxis angesehen und sollte über die entsprechenden Entwurfspraktiken vermieden werden.

Orion Adrian
quelle
2
Könnten Sie die "geeigneten Entwurfspraktiken" beschreiben?
fklappan
6
import com.thoughtworks.xstream.XStream;

public class deepCopy {
    private static  XStream xstream = new XStream();

    //serialize with Xstream them deserialize ...
    public static Object deepCopy(Object obj){
        return xstream.fromXML(xstream.toXML(obj));
    }
}
Eric Leschinski
quelle
5

Ich habe Dozer zum Klonen von Java-Objekten verwendet und es ist großartig, dass die Kryo- Bibliothek eine weitere großartige Alternative ist.

Supernova
quelle
2

BeanUtils macht einen wirklich guten Job beim tiefen Klonen von Bohnen.

BeanUtils.cloneBean(obj);
Alfergon
quelle
4
Es macht flaches Klonen.
Peter Šály
2

1)

public static Object deepClone(Object object) {
   try {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     ObjectOutputStream oos = new ObjectOutputStream(baos);
     oos.writeObject(object);
     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
     ObjectInputStream ois = new ObjectInputStream(bais);
     return ois.readObject();
   }
   catch (Exception e) {
     e.printStackTrace();
     return null;
   }
 }

2)

    // (1) create a MyPerson object named Al
    MyAddress address = new MyAddress("Vishrantwadi ", "Pune", "India");
    MyPerson al = new MyPerson("Al", "Arun", address);

    // (2) make a deep clone of Al
    MyPerson neighbor = (MyPerson)deepClone(al);

Hier müssen Ihre MyPerson- und MyAddress-Klassen eine serilazierbare Schnittstelle implementieren

Arun
quelle
2

Verwenden von Jackson zum Serialisieren und Deserialisieren des Objekts. Für diese Implementierung muss das Objekt die Serializable-Klasse nicht implementieren.

  <T> T clone(T object, Class<T> clazzType) throws IOException {

    final ObjectMapper objMapper = new ObjectMapper();
    String jsonStr= objMapper.writeValueAsString(object);

    return objMapper.readValue(jsonStr, clazzType);

  }
Karthik Rao
quelle