Wie überschreibe ich die Klonmethode richtig?

114

Ich muss einen tiefen Klon in einem meiner Objekte implementieren, der keine Oberklasse hat.

Was ist der beste Weg, um mit dem CloneNotSupportedExceptionvon der Oberklasse (was ist Object) geworfenen Scheck umzugehen ?

Ein Mitarbeiter hat mir geraten, folgendermaßen damit umzugehen:

@Override
public MyObject clone()
{
    MyObject foo;
    try
    {
        foo = (MyObject) super.clone();
    }
    catch (CloneNotSupportedException e)
    {
        throw new Error();
    }

    // Deep clone member fields here

    return foo;
}

Dies scheint mir eine gute Lösung zu sein, aber ich wollte sie der StackOverflow-Community zur Verfügung stellen, um zu prüfen, ob ich weitere Erkenntnisse einschließen kann. Vielen Dank!

Cuga
quelle
6
Wenn Sie wissen, dass die übergeordnete Klasse implementiert, Cloneableist das Auslösen einer AssertionErrorEbene und nicht nur einer Ebene Erroretwas ausdrucksvoller.
Andrew Duffy
Bearbeiten: Ich würde lieber nicht clone () verwenden, aber das Projekt basiert bereits darauf und es wäre nicht wert, alle Referenzen an dieser Stelle zu überarbeiten.
Cuga
Es ist besser, jetzt als später in die Kugel zu beißen (naja, es sei denn, Sie stehen kurz vor der Produktion).
Tom Hawtin - Tackline
Wir haben noch anderthalb Monate auf dem Plan, bis dies abgeschlossen ist. Wir können die Zeit nicht rechtfertigen.
Cuga
Ich denke, diese Lösung ist perfekt. Und fühlen Sie sich nicht schlecht, wenn Sie Klone verwenden. Wenn Sie eine Klasse haben, die als Transportobjekt verwendet wird und nur Primitive und Unveränderliche wie String und Enums enthält, wäre alles andere als Klon aus dogmatischen Gründen eine völlige Zeitverschwendung. Denken Sie daran, was der Klon macht und was nicht! (kein tiefes Klonen)
TomWolk

Antworten:

125

Müssen Sie unbedingt verwenden clone? Die meisten Leute sind sich einig, dass Java clonekaputt ist.

Josh Bloch über Design - Kopierkonstruktor versus Klonen

Wenn Sie den Artikel über das Klonen in meinem Buch gelesen haben, insbesondere wenn Sie zwischen den Zeilen lesen, werden Sie wissen, dass ich denke, dass er clonezutiefst gebrochen ist. [...] Es ist eine Schande, Cloneabledie kaputt ist, aber es passiert.

Weitere Informationen zu diesem Thema finden Sie in seinem Buch Effective Java 2nd Edition, Punkt 11: Mit Bedacht cloneüberschreiben . Er empfiehlt stattdessen die Verwendung eines Kopierkonstruktors oder einer Kopierfabrik.

Er fuhr fort, Seiten von Seiten darüber zu schreiben, wie Sie implementieren sollten, wenn Sie das Gefühl haben, dass Sie es müssen clone. Aber er schloss damit:

Ist all diese Komplexität wirklich notwendig? Selten. Wenn Sie eine Klasse erweitern , dass Geräte Cloneable, Sie kaum eine andere Wahl haben , aber eine gut erzogene zu implementieren cloneMethode. Andernfalls ist es besser, alternative Mittel zum Kopieren von Objekten bereitzustellen oder die Funktion einfach nicht bereitzustellen .

Der Schwerpunkt lag bei ihm, nicht bei mir.


Da Sie klargestellt haben, dass Sie keine andere Wahl haben, als zu implementieren clone, können Sie in diesem Fall Folgendes tun: Stellen Sie sicher, dass MyObject extends java.lang.Object implements java.lang.Cloneable. Wenn dies der Fall ist, können Sie garantieren, dass Sie NIEMALS einen fangen werden CloneNotSupportedException. Werfen, AssertionErrorwie einige vorgeschlagen haben, erscheint vernünftig, aber Sie können auch einen Kommentar hinzufügen, der erklärt, warum der catch-Block in diesem speziellen Fall niemals eingegeben wird .


Alternativ können Sie, wie andere ebenfalls vorgeschlagen haben, möglicherweise implementieren, cloneohne aufzurufen super.clone.

Polygenschmierstoffe
quelle
1
Leider ist das Projekt bereits mit der Klonmethode herumgeschrieben, sonst würde ich es absolut nicht verwenden. Ich stimme Ihnen voll und ganz zu, dass Javas Implementierung des Klons fakakta ist.
Cuga
5
Wenn eine Klasse und alle ihre Oberklassen super.clone()innerhalb ihrer Klonmethoden aufgerufen werden, muss eine Unterklasse im Allgemeinen nur überschreiben, clone()wenn sie neue Felder hinzufügt, deren Inhalt geklont werden müsste. Wenn eine Oberklasse newanstelle von verwendet super.clone(), müssen alle Unterklassen überschreiben, clone()ob sie neue Felder hinzufügen oder nicht.
Supercat
56

Manchmal ist es einfacher, einen Kopierkonstruktor zu implementieren:

public MyObject (MyObject toClone) {
}

Dies erspart Ihnen die Bearbeitung CloneNotSupportedException, arbeitet mit finalFeldern und Sie müssen sich keine Gedanken über den Typ machen, der zurückgegeben werden soll.

Aaron Digulla
quelle
11

Die Art und Weise, wie Ihr Code funktioniert, kommt der "kanonischen" Schreibweise ziemlich nahe. Ich würde aber einen AssertionErrorin den Haken werfen . Es signalisiert, dass diese Linie niemals erreicht werden sollte.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Chris Jester-Young
quelle
In der Antwort von @polygenelubricants erfahren Sie, warum dies eine schlechte Idee sein könnte.
Karl Richter
@ KarlRichter Ich habe ihre Antwort gelesen. Es heißt nicht, dass es eine schlechte Idee ist, außer dass dies Cloneableim Allgemeinen eine kaputte Idee ist, wie in Effective Java erläutert. Das OP hat bereits zum Ausdruck gebracht, dass sie verwenden müssen Cloneable. Ich habe also keine Ahnung, wie ich meine Antwort verbessern könnte, außer sie vielleicht sofort zu löschen.
Chris Jester-Young
9

Es gibt zwei Fälle, in denen der CloneNotSupportedExceptionWurf geworfen wird:

  1. Die zu klonende Klasse wird nicht implementiert Cloneable(vorausgesetzt, das tatsächliche Klonen verschiebt sich schließlich auf die Klonmethode Object). Wenn die Klasse, in der Sie diese Methode schreiben, implementiert ist Cloneable, wird dies niemals passieren (da Unterklassen sie entsprechend erben werden).
  2. Die Ausnahme wird explizit von einer Implementierung ausgelöst. Dies ist die empfohlene Methode, um die Klonbarkeit in einer Unterklasse zu verhindern, wenn sich die Oberklasse befindet Cloneable.

Der letztere Fall kann in Ihrer Klasse nicht auftreten (da Sie die Methode der Oberklasse im tryBlock direkt aufrufen , selbst wenn sie von einem Unterklassenaufruf aufgerufen wird super.clone()), und der erstere sollte dies nicht tun, da Ihre Klasse eindeutig implementieren sollte Cloneable.

Grundsätzlich sollten Sie den Fehler auf jeden Fall protokollieren, aber in diesem speziellen Fall tritt er nur auf, wenn Sie die Definition Ihrer Klasse durcheinander bringen. Behandeln Sie es also wie eine überprüfte Version von NullPointerException(oder ähnlichem) - es wird niemals ausgelöst, wenn Ihr Code funktionsfähig ist.


In anderen Situationen müssten Sie auf diese Eventualität vorbereitet sein - es gibt keine Garantie dafür, dass ein bestimmtes Objekt klonbar ist. Wenn Sie also die Ausnahme abfangen, sollten Sie abhängig von dieser Bedingung geeignete Maßnahmen ergreifen (fahren Sie mit dem vorhandenen Objekt fort und ergreifen Sie eine alternative Klonstrategie zB serialize-deserialize, werfen Sie ein, IllegalParameterExceptionwenn Ihre Methode den Parameter durch klonbar benötigt, etc. etc.).

Bearbeiten : Obwohl ich insgesamt darauf hinweisen sollte, dass es clone()wirklich schwierig ist, richtig zu implementieren, und es für Anrufer schwierig ist zu wissen, ob der Rückgabewert dem entspricht, was sie wollen, doppelt, wenn man tiefe oder flache Klone betrachtet. Es ist oft besser, das Ganze komplett zu vermeiden und einen anderen Mechanismus zu verwenden.

Andrzej Doyle
quelle
Wenn ein Objekt eine öffentliche Klonmethode verfügbar macht, verstößt jedes abgeleitete Objekt, das es nicht unterstützt, gegen das Liskov-Substitutionsprinzip. Wenn eine Klonmethode geschützt ist, würde ich denken, dass es besser ist, sie mit etwas anderem als einer Methode zu beschatten, die den richtigen Typ zurückgibt, um zu verhindern, dass eine Unterklasse überhaupt versucht, sie aufzurufen super.clone().
Supercat
5

Verwenden Sie die Serialisierung , um tiefe Kopien zu erstellen. Dies ist nicht die schnellste Lösung, hängt jedoch nicht vom Typ ab.

Michał Ziober
quelle
Dies war eine großartige und offensichtliche Lösung, wenn man bedenkt, dass ich bereits GSON verwendet habe.
Jacob Tabak
3

Sie können geschützte Kopierkonstruktoren wie folgt implementieren:

/* This is a protected copy constructor for exclusive use by .clone() */
protected MyObject(MyObject that) {
    this.myFirstMember = that.getMyFirstMember(); //To clone primitive data
    this.mySecondMember = that.getMySecondMember().clone(); //To clone complex objects
    // etc
}

public MyObject clone() {
    return new MyObject(this);
}
Dolph
quelle
3
Dies funktioniert meistens nicht. Sie können clone()das von zurückgegebene Objekt nur aufrufen, getMySecondMember()wenn es über eine public cloneMethode verfügt.
polygenelubricants
Natürlich müssen Sie dieses Muster für jedes Objekt implementieren, das Sie klonen möchten, oder einen anderen Weg finden, um jedes Mitglied tief zu klonen.
Dolph
Ja, das ist eine notwendige Voraussetzung.
Polygenelubricants
1

So sehr die meisten Antworten hier gültig sind, muss ich sagen, dass Ihre Lösung auch so ist, wie es die tatsächlichen Java-API-Entwickler tun. (Entweder Josh Bloch oder Neal Gafter)

Hier ist ein Auszug aus der ArrayList-Klasse openJDK:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

Wie Sie bemerkt haben und andere erwähnt haben, CloneNotSupportedExceptionhat es fast keine Chance, geworfen zu werden, wenn Sie erklärt haben, dass Sie die CloneableSchnittstelle implementieren .

Außerdem müssen Sie die Methode nicht überschreiben, wenn Sie in der überschriebenen Methode nichts Neues tun. Sie müssen es nur überschreiben, wenn Sie zusätzliche Vorgänge für das Objekt ausführen oder es öffentlich machen müssen.

Letztendlich ist es immer noch am besten, dies zu vermeiden und es auf eine andere Weise zu tun.

Haggra
quelle
0
public class MyObject implements Cloneable, Serializable{   

    @Override
    @SuppressWarnings(value = "unchecked")
    protected MyObject clone(){
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            ByteArrayOutputStream bOs = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bOs);
            oos.writeObject(this);
            ois = new ObjectInputStream(new ByteArrayInputStream(bOs.toByteArray()));
            return  (MyObject)ois.readObject();

        } catch (Exception e) {
            //Some seriouse error :< //
            return null;
        }finally {
            if (oos != null)
                try {
                    oos.close();
                } catch (IOException e) {

                }
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {

                }
        }
    }
}
Kvant_hv
quelle
Sie haben es also gerade in das Dateisystem geschrieben und das Objekt zurückgelesen. Okay, ist dies die beste Methode, um mit Klonen umzugehen? Kann jemand aus der SO-Community diesen Ansatz kommentieren? Ich denke, dies verbindet unnötig das Klonen und die Serialisierung - zwei völlig unterschiedliche Konzepte. Ich werde abwarten, was andere dazu zu sagen haben.
Saurabh Patil
2
Es befindet sich nicht im Dateisystem, sondern im Hauptspeicher (ByteArrayOutputStream). Für tief verschachtelte Objekte ist es die Lösung, die gut funktioniert. Besonders wenn Sie es nicht oft brauchen, zB im Unit Test. wo Leistung nicht das Hauptziel ist.
AlexWien
0

Nur weil die Java-Implementierung von Cloneable fehlerhaft ist, heißt das nicht, dass Sie keine eigene erstellen können.

Wenn OP der eigentliche Zweck war, einen tiefen Klon zu erstellen, denke ich, dass es möglich ist, eine Schnittstelle wie diese zu erstellen:

public interface Cloneable<T> {
    public T getClone();
}

Verwenden Sie dann den zuvor erwähnten Prototyp-Konstruktor, um ihn zu implementieren:

public class AClass implements Cloneable<AClass> {
    private int value;
    public AClass(int value) {
        this.vaue = value;
    }

    protected AClass(AClass p) {
        this(p.getValue());
    }

    public int getValue() {
        return value;
    }

    public AClass getClone() {
         return new AClass(this);
    }
}

und eine andere Klasse mit einem AClass-Objektfeld:

public class BClass implements Cloneable<BClass> {
    private int value;
    private AClass a;

    public BClass(int value, AClass a) {
         this.value = value;
         this.a = a;
    }

    protected BClass(BClass p) {
        this(p.getValue(), p.getA().getClone());
    }

    public int getValue() {
        return value;
    }

    public AClass getA() {
        return a;
    }

    public BClass getClone() {
         return new BClass(this);
    }
}

Auf diese Weise können Sie ein Objekt der Klasse BClass problemlos tief klonen, ohne @SuppressWarnings oder anderen kniffligen Code zu benötigen.

Francesco Rogo
quelle