Come sovrascrivere correttamente il metodo di clonazione?


114

Devo implementare un clone profondo in uno dei miei oggetti che non ha una superclasse.

Qual è il modo migliore per gestire il check CloneNotSupportedExceptionlanciato dalla superclasse (che è Object)?

Un collega mi ha consigliato di gestirlo nel modo seguente:

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

    // Deep clone member fields here

    return foo;
}

Questa mi sembra una buona soluzione, ma volevo inviarla alla community di StackOverflow per vedere se ci sono altri approfondimenti che posso includere. Grazie!


6
Se sai che la classe genitore implementa, Cloneablelanciare un AssertionErrorpiuttosto che un semplice Errorè un po 'più espressivo.
Andrew Duffy

Modifica: preferirei non usare clone (), ma il progetto è già basato su di esso e non varrebbe la pena refactoring di tutti i riferimenti a questo punto.
Cuga

Meglio stringere i denti ora piuttosto che dopo (beh, a meno che tu non stia per iniziare la produzione).
Tom Hawtin - tackline

Abbiamo ancora un mese e mezzo in programma fino al completamento. Non possiamo giustificare il tempo.
Cuga

Penso che questa soluzione sia perfetta. E non sentirti male per l'utilizzo di clone. Se hai una classe usata come oggetto di trasporto e contiene solo primitve e immutabili come String ed Enums, tutto il resto oltre a clone sarebbe una completa perdita di tempo per ragioni dogmatiche. Tieni a mente cosa fa il clone e cosa no! (nessuna clonazione profonda)
TomWolk

Risposte:


125

Devi assolutamente usare clone? La maggior parte delle persone concorda sul fatto che Java clonesia rotto.

Josh Bloch su Design - Copy Constructor vs Cloning

Se hai letto l'articolo sulla clonazione nel mio libro, soprattutto se leggi tra le righe, saprai che secondo me cloneè profondamente rotto. [...] È un peccato che Cloneablesia rotto, ma succede.

Puoi leggere ulteriori discussioni sull'argomento nel suo libro Effective Java 2nd Edition, Item 11: Override clonejudiciously . Raccomanda invece di utilizzare un costruttore di copie o una fabbrica di copie.

Ha continuato a scrivere pagine di pagine su come, se ritieni di doverlo fare, dovresti implementare clone. Ma ha chiuso con questo:

Tutte queste complessità sono davvero necessarie? Raramente. Se estendi una classe che implementa Cloneable, non hai altra scelta che implementare un clonemetodo ben comportato . Altrimenti, è meglio fornire mezzi alternativi per la copia di oggetti o semplicemente non fornire la capacità .

L'enfasi era sua, non mia.


Dato che hai chiarito che non hai altra scelta che implementare clone, ecco cosa puoi fare in questo caso: assicurati che MyObject extends java.lang.Object implements java.lang.Cloneable. Se è così, puoi garantire che non prenderai MAI un file CloneNotSupportedException. Lanciare AssertionErrorcome alcuni hanno suggerito sembra ragionevole, ma puoi anche aggiungere un commento che spiega perché il blocco catch non verrà mai inserito in questo caso particolare .


In alternativa, come hanno suggerito anche altri, puoi magari implementare clonesenza chiamare super.clone.


1
Purtroppo il progetto è già scritto in giro usando il metodo clone, altrimenti non lo userei assolutamente. Sono completamente d'accordo con te sul fatto che l'implementazione di Java di clone sia fakakta.
Cuga

5
Se una classe e tutte le sue superclassi chiamano super.clone()all'interno dei loro metodi clone, una sottoclasse generalmente dovrà sovrascrivere solo clone()se aggiunge nuovi campi il cui contenuto dovrebbe essere clonato. Se una qualsiasi superclasse utilizza newinvece di super.clone(), tutte le sottoclassi devono sovrascrivere clone()se aggiungono o meno nuovi campi.
supercat

56

A volte è più semplice implementare un costruttore di copie:

public MyObject (MyObject toClone) {
}

Ti risparmia la fatica di maneggiare CloneNotSupportedException, lavora con i finalcampi e non devi preoccuparti del tipo da restituire.


11

Il modo in cui funziona il tuo codice è molto vicino al modo "canonico" di scriverlo. Però ne getterei una AssertionErrordentro la presa. Segnala che quella linea non dovrebbe mai essere raggiunta.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}

Vedi la risposta di @polygenelubricants per capire perché questa potrebbe essere una cattiva idea.
Karl Richter,

@KarlRichter ho letto la loro risposta. Non dice che è una cattiva idea, a parte che Cloneablein generale è un'idea non funzionante, come spiegato in Effective Java. L'OP ha già espresso che devono usare Cloneable. Quindi non ho idea in quale altro modo la mia risposta potrebbe essere migliorata, tranne forse cancellandola del tutto.
Chris Jester-Young,

9

Ci sono due casi in cui CloneNotSupportedExceptionverrà lanciato:

  1. La classe da clonare non viene implementata Cloneable(supponendo che la clonazione effettiva alla fine rimanda al Objectmetodo di clonazione di). Se la classe su cui stai scrivendo questo metodo in implements Cloneable, ciò non accadrà mai (poiché qualsiasi sottoclasse lo erediterà in modo appropriato).
  2. L'eccezione viene lanciata esplicitamente da un'implementazione: questo è il modo consigliato per prevenire la clonabilità in una sottoclasse quando la superclasse lo è Cloneable.

Quest'ultimo caso non può verificarsi nella tua classe (poiché stai chiamando direttamente il metodo della superclasse nel tryblocco, anche se invocato da una chiamata di sottoclasse super.clone()) e il primo non dovrebbe poiché la tua classe dovrebbe chiaramente implementare Cloneable.

Fondamentalmente, dovresti sicuramente registrare l'errore, ma in questo caso particolare accadrà solo se sbagli la definizione della tua classe. Quindi trattalo come una versione verificata di NullPointerException(o simile): non verrà mai lanciato se il tuo codice è funzionante.


In altre situazioni dovresti essere preparato per questa eventualità: non c'è garanzia che un determinato oggetto sia clonabile, quindi quando si cattura l'eccezione è necessario intraprendere un'azione appropriata a seconda di questa condizione (continuare con l'oggetto esistente, adottare una strategia di clonazione alternativa es. serializzare-deserializzare, lanciare un IllegalParameterExceptionse il metodo richiede il parametro da clonabile, ecc. ecc.).

Modifica : anche se nel complesso dovrei sottolineare che sì, clone()è davvero difficile da implementare correttamente e difficile per i chiamanti sapere se il valore di ritorno sarà quello che vogliono, doppiamente quando si considerano i cloni profondi o superficiali. Spesso è meglio evitare del tutto tutto e utilizzare un altro meccanismo.


Se un oggetto espone un metodo di clonazione pubblico, qualsiasi oggetto derivato che non lo supporta violerebbe il principio di sostituzione di Liskov. Se un metodo di clonazione è protetto, penserei che sarebbe meglio ombreggiarlo con qualcosa di diverso da un metodo che restituisce il tipo corretto, per evitare che una sottoclasse provi a chiamare super.clone().
supercat


3

Puoi implementare costruttori di copie protette in questo modo:

/* 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);
}

3
Questo non funziona la maggior parte del tempo. Non puoi chiamare clone()sull'oggetto restituito da a getMySecondMember()meno che non abbia un public clonemetodo.
lubrificanti poligenici

Ovviamente, devi implementare questo modello su ogni oggetto che vuoi essere in grado di clonare, o trovare un altro modo per clonare in profondità ogni membro.
Dolph

Sì, questo è un requisito necessario.
lubrificanti poligenici

1

Per quanto la maggior parte delle risposte qui siano valide, devo dire che la tua soluzione è anche il modo in cui lo fanno gli attuali sviluppatori di API Java. (O Josh Bloch o Neal Gafter)

Ecco un estratto da openJDK, classe ArrayList:

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

Come avrai notato e altri menzionati, non CloneNotSupportedExceptionha quasi alcuna possibilità di essere lanciato se hai dichiarato di implementare l' Cloneableinterfaccia.

Inoltre, non è necessario sovrascrivere il metodo se non si esegue nulla di nuovo nel metodo sovrascritto. È necessario sovrascriverlo solo quando è necessario eseguire operazioni aggiuntive sull'oggetto o è necessario renderlo pubblico.

In definitiva, è comunque meglio evitarlo e farlo in un altro modo.


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) {

                }
        }
    }
}

Quindi l'hai appena scritto nel file system e hai riletto l'oggetto. Va bene, questo è il metodo migliore per gestire il clone? Qualcuno della comunità SO può commentare questo approccio? Immagino che questo leghi inutilmente Clonazione e Serializzazione, due concetti completamente diversi. Aspetterò di vedere cosa hanno da dire gli altri al riguardo.
Saurabh Patil

2
Non è nel file system, è nella memoria principale (ByteArrayOutputStream). per oggetti profondamente nidificati è la soluzione che funziona bene. Soprattutto se non ne hai bisogno spesso, ad esempio in unit test. dove le prestazioni non sono l'obiettivo principale.
AlexWien

0

Solo perché l'implementazione di java di Cloneable è interrotta, non significa che non puoi crearne uno tuo.

Se il vero scopo di OP fosse creare un clone profondo, penso che sia possibile creare un'interfaccia come questa:

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

quindi utilizzare il costruttore del prototipo menzionato prima per implementarlo:

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

e un'altra classe con un campo oggetto AClass:

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

In questo modo puoi clonare facilmente in profondità un oggetto della classe BClass senza bisogno di @SuppressWarnings o altro codice ingannevole.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.