clone () vs copy constructor vs factory method?

81

Ich habe bei der Implementierung von clone () in Java schnell googelt und festgestellt: http://www.javapractices.com/topic/TopicAction.do?Id=71

Es hat den folgenden Kommentar:

Kopierkonstruktoren und statische Factory-Methoden bieten eine Alternative zum Klonen und sind viel einfacher zu implementieren.

Ich möchte nur eine tiefe Kopie erstellen. Die Implementierung von clone () scheint sehr sinnvoll zu sein, aber dieser Artikel mit hohem Google-Rang macht mir ein bisschen Angst.

Hier sind die Probleme, die mir aufgefallen sind:

Kopierkonstruktoren funktionieren nicht mit Generics.

Hier ist ein Pseudocode, der nicht kompiliert werden kann.

public class MyClass<T>{
   ..
   public void copyData(T data){
       T copy=new T(data);//This isn't going to work.    
   }
   ..
}

Beispiel 1: Verwenden eines Kopierkonstruktors in einer generischen Klasse.

Factory-Methoden haben keine Standardnamen.

Es ist sehr schön, eine Schnittstelle für wiederverwendbaren Code zu haben.

public class MyClass<T>{
    ..
    public void copyData(T data){
        T copy=data.clone();//Throws an exception if the input was not cloneable
    }
    ..
}

Beispiel 2: Verwenden von clone () in einer generischen Klasse.

Ich habe festgestellt, dass das Klonen keine statische Methode ist, aber wäre es nicht immer noch notwendig, tiefe Kopien aller geschützten Felder zu erstellen? Bei der Implementierung von clone () erscheint mir der zusätzliche Aufwand, Ausnahmen in nicht klonbaren Unterklassen auszulösen, trivial.

Vermisse ich etwas Alle Einblicke wäre dankbar.

Benutzer1
quelle

Antworten:

52

Grundsätzlich ist der Klon kaputt . Mit Generika funktioniert nichts. Wenn Sie so etwas haben (verkürzt, um den Punkt zu vermitteln):

public class SomeClass<T extends Copyable> {


    public T copy(T object) {
        return (T) object.copy();
    }
}

interface Copyable {
    Copyable copy();
}

Dann können Sie mit einer Compiler-Warnung die Arbeit erledigen. Da Generika zur Laufzeit gelöscht werden, wird in etwas, das eine Kopie erstellt, eine Compiler-Warnung generiert. Dies ist in diesem Fall nicht vermeidbar. . Es ist in einigen Fällen vermeidbar (danke, kb304), aber nicht in allen. Stellen Sie sich den Fall vor, in dem Sie eine Unterklasse oder eine unbekannte Klasse unterstützen müssen, die die Schnittstelle implementiert (z. B. wenn Sie eine Sammlung kopierbarer Dateien durchlaufen haben, die nicht unbedingt dieselbe Klasse generiert haben).

Yishai
quelle
2
Es ist auch möglich zu tun , ohne Compiler - Warnung.
Akarnokd
@ kd304, wenn du meinst, dass du die Compiler-Warnung unterdrückst, wahr, aber wirklich nicht anders. Wenn Sie meinen, dass Sie die Notwendigkeit einer Warnung ganz vermeiden können, erläutern Sie dies bitte.
Yishai
@ Yishai: Schau dir meine Antwort an. Und ich bezog mich nicht auf die Unterdrückungswarnung.
Akarnokd
Downvoting, weil Copyable<T>die Antwort in kd304 viel sinnvoller ist.
Simon Forsberg
25

Es gibt auch das Builder-Muster. Weitere Informationen finden Sie unter Effektives Java.

Ich verstehe Ihre Bewertung nicht. In einem Kopierkonstruktor kennen Sie den Typ genau. Warum müssen Generika verwendet werden?

public class C {
   public int value;
   public C() { }
   public C(C other) {
     value = other.value;
   }
}

Vor kurzem gab es hier eine ähnliche Frage .

public class G<T> {
   public T value;
   public G() { }
   public G(G<? extends T> other) {
     value = other.value;
   }
}

Ein lauffähiges Beispiel:

public class GenTest {
    public interface Copyable<T> {
        T copy();
    }
    public static <T extends Copyable<T>> T copy(T object) {
        return object.copy();
    }
    public static class G<T> implements Copyable<G<T>> {
        public T value;
        public G() {
        }
        public G(G<? extends T> other) {
            value = other.value;
        }
        @Override
        public G<T> copy() {
            return new G<T>(this);
        }
    }
    public static void main(String[] args) {
        G<Integer> g = new G<Integer>();
        g.value = 1;
        G<Integer> f = g.copy();
        g.value = 2;
        G<Integer> h = copy(g);
        g.value = 3;
        System.out.printf("f: %s%n", f.value);
        System.out.printf("g: %s%n", g.value);
        System.out.printf("h: %s%n", h.value);
    }
}
akarnokd
quelle
1
+1 Dies ist der einfachste, aber effektivste Weg, um einen Kopierkonstruktor zu implementieren
dfa
Beachten Sie, dass MyClass oben eine generische ist, StackOverflow hat das <T> verschluckt.
User1
Die Formatierung Ihrer Fragen wurde korrigiert. Verwenden Sie in jeder Zeile vier Leerzeichen anstelle von Pre + Code-Tags.
Akarnokd
Ich erhalte eine Fehlermeldung über die Kopiermethode von G - "muss eine Superklassenmethode überschreiben"
Carl Pritchett
3
(Ich weiß, dass dies alt ist, aber für die Nachwelt) @CarlPritchett: Dieser Fehler wird in Java 1.5 und darunter angezeigt. Das Markieren von Schnittstellenmethoden als @ Override ist ab Java 1.6 zulässig.
Carrotman42
10

Java hat keine Kopierkonstruktoren im gleichen Sinne wie C ++.

Sie können einen Konstruktor haben, der ein Objekt des gleichen Typs als Argument verwendet, aber nur wenige Klassen unterstützen dies. (weniger als die Anzahl, die das Klonen unterstützen)

Für einen generischen Klon habe ich eine Hilfsmethode, die eine neue Instanz einer Klasse erstellt und die Felder aus dem Original (eine flache Kopie) mithilfe von Reflexionen kopiert (eigentlich so etwas wie Reflexionen, aber schneller).

Für eine tiefe Kopie besteht ein einfacher Ansatz darin, das Objekt zu serialisieren und es zu de-serialisieren.

Übrigens: Mein Vorschlag ist, unveränderliche Objekte zu verwenden, dann müssen Sie sie nicht klonen. ;)

Peter Lawrey
quelle
Könnten Sie bitte erklären, warum ich kein Klonen benötige, wenn ich unveränderliche Objekte verwende? In meiner Anwendung muss ein anderes Objekt vorhanden sein, das genau dieselben Daten wie das vorhandene Objekt enthält. Also muss ich es irgendwie kopieren
Zhenya
2
@Ievgen Wenn Sie zwei Verweise auf dieselben Daten benötigen, können Sie den Verweis kopieren. Sie müssen den Inhalt eines Objekts nur kopieren, wenn er sich ändern könnte (aber für unveränderliche Objekte wissen Sie, dass dies nicht der
Fall ist
Wahrscheinlich verstehe ich die Vorteile unveränderlicher Objekte einfach nicht. Angenommen, ich habe eine JPA- / Ruhezustandsentität und muss eine andere JPA-Entität basierend auf der vorhandenen erstellen, aber ich muss die ID der neuen Entität ändern. Wie würde ich es mit unveränderlichen Objekten machen?
Zhenya
@Ievgen per Definition können Sie unveränderliche Objekte nicht ändern. StringZum Beispiel ist ein unveränderliches Objekt und Sie können einen String weitergeben, ohne ihn kopieren zu müssen.
Peter Lawrey
6

Ich denke, die Antwort von Yishai könnte verbessert werden, sodass wir mit dem folgenden Code keine Warnung erhalten können:

public class SomeClass<T extends Copyable<T>> {

    public T copy(T object) {
        return object.copy();
    }
}

interface Copyable<T> {
    T copy();
}

Auf diese Weise muss eine Klasse, die eine kopierbare Schnittstelle implementieren muss, folgendermaßen aussehen:

public class MyClass implements Copyable<MyClass> {

    @Override
    public MyClass copy() {
        // copy implementation
        ...
    }

}
Alepac
quelle
2
Fast. Die kopierbare Schnittstelle sollte wie folgt deklariert werden: Dies entspricht interface Copyable<T extends Copyable>am ehesten einem Selbsttyp , den Sie in Java codieren können.
Recurse
Natürlich hast du das gemeint interface Copyable<T extends Copyable<T>>, oder? ;)
Adowrath
3

Im Folgenden sind einige Nachteile aufgeführt, die viele Entwickler nicht verwenden Object.clone()

  1. Die Verwendung von Object.clone()method erfordert, dass wir unserem Code viel Syntax hinzufügen, z. B. die CloneableSchnittstelle implementieren , clone()Methode und Handle definieren CloneNotSupportedExceptionund schließlich Object.clone()unser Objekt aufrufen und umwandeln .
  2. CloneableSchnittstelle fehlt clone()Methode, es ist eine Markierungsschnittstelle und enthält keine Methode, und dennoch müssen wir sie implementieren, um JVM mitzuteilen, dass wir clone()auf unserem Objekt ausführen können .
  3. Object.clone()ist geschützt, so dass wir unseren eigenen clone()und indirekten Anruf Object.clone()von ihm bereitstellen müssen .
  4. Wir haben keine Kontrolle über die Objektkonstruktion, da Object.clone()kein Konstruktor aufgerufen wird.
  5. Wenn wir eine clone()Methode in eine untergeordnete Klasse schreiben , z. B. sollten Personalle ihre Oberklassen eine clone()Methode in ihnen definieren oder sie von einer anderen übergeordneten Klasse erben, da sonst die super.clone()Kette fehlschlägt.
  6. Object.clone()unterstützt nur flache Kopien, sodass Referenzfelder unseres neu geklonten Objekts weiterhin Objekte enthalten, die Felder unseres ursprünglichen Objekts enthielten. Um dies zu überwinden, müssen wir clone()in jeder Klasse, deren Referenz unsere Klasse hält , implementieren und sie dann in unserer clone()Methode wie im folgenden Beispiel separat klonen .
  7. Wir können die endgültigen Felder nicht bearbeiten, Object.clone()da die endgültigen Felder nur über Konstruktoren geändert werden können. In unserem Fall erhalten wir, wenn wir möchten, dass jedes PersonObjekt durch die ID eindeutig ist, das doppelte Objekt, wenn wir es verwenden, Object.clone()da Object.clone()der Konstruktor nicht aufgerufen wird und das letzte idFeld nicht geändert werden kann Person.clone().

Kopierkonstruktoren sind besser als Object.clone()weil sie

  1. Zwingen Sie uns nicht, eine Schnittstelle zu implementieren oder eine Ausnahme auszulösen, aber wir können dies sicherlich tun, wenn dies erforderlich ist.
  2. Benötige keine Abgüsse.
  3. Wir müssen uns nicht auf einen unbekannten Objekterstellungsmechanismus verlassen.
  4. Es ist nicht erforderlich, dass die Elternklasse einem Vertrag folgt oder etwas implementiert.
  5. Erlauben Sie uns, die letzten Felder zu ändern.
  6. Damit wir die vollständige Kontrolle über die Objekterstellung haben, können wir unsere Initialisierungslogik darin schreiben.

Lesen Sie mehr über Java Cloning - Copy Constructor versus Cloning

Naresh Joshi
quelle
1

Normalerweise arbeitet clone () mit einem geschützten Kopierkonstruktor zusammen. Dies geschieht, weil clone () im Gegensatz zu einem Konstruktor virtuell sein kann.

In einem Klassenkörper für Abgeleitet von einer Superklassenbasis haben wir

class Derived extends Base {
}

Im einfachsten Fall würden Sie also einen virtuellen Kopierkonstruktor mit dem clone () hinzufügen. (In C ++ empfiehlt Joshi das Klonen als Konstruktor für virtuelle Kopien.)

protected Derived() {
    super();
}

protected Object clone() throws CloneNotSupportedException {
    return new Derived();
}

Es wird komplizierter, wenn Sie super.clone () wie empfohlen aufrufen möchten und diese Mitglieder zur Klasse hinzufügen müssen. Sie können dies versuchen

final String name;
Address address;

/// This protected copy constructor - only constructs the object from super-class and
/// sets the final in the object for the derived class.
protected Derived(Base base, String name) {
   super(base);
   this.name = name;
}

protected Object clone() throws CloneNotSupportedException {
    Derived that = new Derived(super.clone(), this.name);
    that.address = (Address) this.address.clone();
}

Nun, wenn eine Hinrichtung, haben Sie

Base base = (Base) new Derived("name");

und du hast es dann getan

Base clone = (Base) base.clone();

Dies würde clone () in der abgeleiteten Klasse (die oben genannte) aufrufen. Dies würde super.clone () aufrufen - was möglicherweise implementiert ist oder nicht, aber es wird empfohlen, es aufzurufen. Die Implementierung übergibt dann die Ausgabe von super.clone () an einen Konstruktor für geschützte Kopien, der eine Basis verwendet, und Sie übergeben alle endgültigen Mitglieder an diese.

Dieser Kopierkonstruktor ruft dann den Kopierkonstruktor der Superklasse auf (wenn Sie wissen, dass er einen hat) und legt das Finale fest.

Wenn Sie zur clone () -Methode zurückkehren, legen Sie alle nicht endgültigen Mitglieder fest.

Kluge Leser werden feststellen, dass ein Kopierkonstruktor in der Basis von super.clone () aufgerufen wird - und erneut aufgerufen wird, wenn Sie den Superkonstruktor im geschützten Konstruktor aufrufen, sodass Sie möglicherweise den aufrufen Super Copy-Konstruktor zweimal. Wenn es Ressourcen sperrt, wird es das hoffentlich wissen.

Bonaparte
quelle
0

Ein Muster, das für Sie möglicherweise funktioniert, ist das Kopieren auf Bean-Ebene. Grundsätzlich verwenden Sie einen Konstruktor ohne Argumente und rufen verschiedene Setter auf, um die Daten bereitzustellen. Sie können sogar die verschiedenen Bean-Eigenschaftsbibliotheken verwenden, um die Eigenschaften relativ einfach festzulegen. Dies ist nicht dasselbe wie ein Klon (), aber für viele praktische Zwecke ist es in Ordnung.

Mr. Shiny und New 安 宇
quelle
0

Die klonbare Schnittstelle ist in dem Sinne defekt, dass sie nutzlos ist, aber gut funktioniert und zu einer besseren Leistung für große Objekte führen kann - 8 Felder und mehr, aber dann wird die Escape-Analyse fehlschlagen. Daher ist es vorzuziehen, die meiste Zeit den Kopierkonstruktor zu verwenden. Die Verwendung von Klon für Array ist schneller als Arrays.copyOf, da die Länge garantiert gleich ist.

Weitere Details finden Sie hier https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html

user3996996
quelle
0

Wenn man sich nicht aller Macken zu 100% bewusst ist clone(), würde ich raten, sich davon fernzuhalten. Ich würde das nicht clone()kaputt sagen . Ich würde sagen: Verwenden Sie es nur, wenn Sie ganz sicher sind, dass es Ihre beste Option ist. Ein Kopierkonstruktor (oder eine Factory-Methode, die meiner Meinung nach nicht wirklich wichtig ist) ist einfach zu schreiben (möglicherweise langwierig, aber einfach). Er kopiert nur das, was Sie kopieren möchten, und kopiert die Art und Weise, wie die Dinge kopiert werden sollen. Sie können es genau auf Ihre Bedürfnisse zuschneiden.

Plus: Es ist einfach zu debuggen, was passiert, wenn Sie Ihre Kopierkonstruktor- / Factory-Methode aufrufen.

Und erstellt clone()nicht sofort eine "tiefe" Kopie Ihres Objekts, vorausgesetzt, Sie meinen, dass nicht nur die Verweise (z. B. auf a Collection) kopiert werden. Aber lesen Sie hier mehr über tief und flach: Tiefe Kopie, flache Kopie, Klon

sorrymissjackson
quelle
-2

Was Sie vermissen, ist, dass der Klon standardmäßig flache Kopien und Konventionen erstellt und dass es im Allgemeinen nicht möglich ist, tiefe Kopien zu erstellen.

Das Problem ist, dass Sie keine tiefen Kopien von zyklischen Objektdiagrammen erstellen können, ohne verfolgen zu können, welche Objekte besucht wurden. clone () bietet kein solches Tracking (da dies ein Parameter für .clone () sein müsste) und erstellt daher nur flache Kopien.

Selbst wenn Ihr eigenes Objekt .clone für alle seine Mitglieder aufruft, ist es keine tiefe Kopie.

Martin v. Löwis
quelle
4
Es kann unmöglich sein, clone () für ein beliebiges Objekt tief zu kopieren, aber in der Praxis ist dies für viele Objekthierarchien verwaltbar. Es hängt nur davon ab, welche Arten von Objekten Sie haben und welche Mitgliedsvariablen sie haben.
Mr. Shiny und New
Es gibt weit weniger Unklarheiten, als viele Menschen behaupten. Wenn man ein klonbares SuperDuperList<T>oder abgeleitetes davon hat, sollte das Klonen eine neue Instanz des gleichen Typs ergeben wie die, die geklont wurde; Es sollte vom Original getrennt sein, sich jedoch Tin derselben Reihenfolge wie das Original auf dasselbe beziehen . Nichts, was von diesem Punkt an an einer der beiden Listen getan wird, sollte die Identität der in der anderen gespeicherten Objekte beeinflussen. Ich kenne keine nützliche "Konvention", die eine generische Sammlung erfordern würde, um ein anderes Verhalten aufzuweisen.
Supercat