Comment remplacer correctement la méthode de clonage?

114

J'ai besoin d'implémenter un clone profond dans l'un de mes objets qui n'a pas de superclasse.

Quelle est la meilleure façon de gérer le CloneNotSupportedExceptionjeté vérifié par la superclasse (qui est Object)?

Un collègue m'a conseillé de le gérer de la manière suivante:

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

    // Deep clone member fields here

    return foo;
}

Cela me semble être une bonne solution, mais je voulais la lancer à la communauté StackOverflow pour voir s'il y a d'autres informations que je peux inclure. Merci!

Cuga
la source
6
Si vous savez que la classe parente implémente, Cloneablealors lancer un AssertionErrorplutôt qu'un simple Errorest un peu plus expressif.
Andrew Duffy
Edit: Je préfère ne pas utiliser clone (), mais le projet est déjà basé dessus et il ne vaudrait pas la peine de refactoriser toutes les références à ce stade.
Cuga
Mieux vaut mordre la balle maintenant plutôt que plus tard (enfin, sauf si vous êtes sur le point de passer à la production).
Tom Hawtin - tackline
Il nous reste un mois et demi sur le calendrier jusqu'à ce que cela soit terminé. Nous ne pouvons pas justifier le temps.
Cuga
Je pense que cette solution est parfaite. Et ne vous sentez pas mal à l'idée d'utiliser le clone. Si vous avez une classe qui est utilisée comme objet de transport et qu'elle ne contient que des primitves et des immuables comme String et Enums, tout autre chose que le clonage serait une perte de temps totale pour des raisons dogmatiques. Gardez simplement à l'esprit ce que fait et non le clone! (pas de clonage profond)
TomWolk

Réponses:

125

Devez-vous absolument utiliser clone? La plupart des gens conviennent que Java cloneest cassé.

Josh Bloch sur la conception - Constructeur de copie versus clonage

Si vous avez lu l'article sur le clonage dans mon livre, surtout si vous lisez entre les lignes, vous saurez que je pense qu'il cloneest profondément brisé. [...] C'est une honte qui Cloneableest cassée, mais ça arrive.

Vous pouvez lire plus de discussion sur le sujet dans son livre Effective Java 2nd Edition, Item 11: Override clonejudicieusement . Il recommande à la place d'utiliser un constructeur de copie ou une fabrique de copies.

Il a continué en écrivant des pages de pages sur la façon dont, si vous pensez que vous devez, vous devriez mettre en œuvre clone. Mais il a conclu avec ceci:

Toutes ces complexités sont-elles vraiment nécessaires? Rarement. Si vous étendez une classe qui implémente Cloneable, vous n'avez guère d'autre choix que d'implémenter une cloneméthode bien conduite . Sinon, il vaut mieux fournir des moyens alternatifs de copie d'objets, ou simplement ne pas fournir la capacité .

L'accent était le sien, pas le mien.


Puisque vous avez clairement indiqué que vous n'aviez pas d'autre choix que de mettre en œuvre clone, voici ce que vous pouvez faire dans ce cas: assurez-vous que MyObject extends java.lang.Object implements java.lang.Cloneable. Si tel est le cas, vous pouvez garantir que vous n'attraperez JAMAIS un CloneNotSupportedException. Lancer AssertionErrorcomme certains l'ont suggéré semble raisonnable, mais vous pouvez également ajouter un commentaire expliquant pourquoi le bloc catch ne sera jamais entré dans ce cas particulier .


Alternativement, comme d'autres l'ont également suggéré, vous pouvez peut-être implémenter clonesans appeler super.clone.

lubrifiants polygènes
la source
1
Malheureusement, le projet est déjà écrit en utilisant la méthode clone, sinon je ne l'utiliserais absolument pas. Je suis entièrement d'accord avec vous pour dire que l'implémentation Java du clone est fakakta.
Cuga
5
Si une classe et toutes ses superclasses appellent super.clone()dans leurs méthodes de clonage, une sous-classe n'aura généralement à remplacer que clone()si elle ajoute de nouveaux champs dont le contenu devrait être cloné. Si une superclasse utilise newplutôt que super.clone(), alors toutes les sous-classes doivent remplacer clone()si elles ajoutent ou non de nouveaux champs.
supercat
56

Parfois, il est plus simple d'implémenter un constructeur de copie:

public MyObject (MyObject toClone) {
}

Cela vous évite les problèmes de manipulation CloneNotSupportedException, fonctionne avec les finalchamps et vous n'avez pas à vous soucier du type à retourner.

Aaron Digulla
la source
11

La façon dont votre code fonctionne est assez proche de la manière «canonique» de l'écrire. Je jetterais un AssertionErrordans la prise, cependant. Cela signale que cette ligne ne doit jamais être atteinte.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Chris Jester-Young
la source
Voir la réponse de @polygenelubricants pour savoir pourquoi cela pourrait être une mauvaise idée.
Karl Richter
@KarlRichter J'ai lu leur réponse. Cela ne dit pas que c'est une mauvaise idée, à part cela, Cloneableen général, c'est une idée cassée, comme expliqué dans Effective Java. Le PO a déjà exprimé qu'ils doivent utiliser Cloneable. Je n'ai donc aucune idée de la manière dont ma réponse pourrait être améliorée, sauf peut-être la supprimer purement et simplement.
Chris Jester-Young
9

Il y a deux cas dans lesquels le CloneNotSupportedExceptionsera lancé:

  1. La classe clonée n'est pas implémentée Cloneable(en supposant que le clonage réel soit finalement reporté à Objectla méthode de clonage de). Si la classe dans laquelle vous écrivez cette méthode implémente Cloneable, cela ne se produira jamais (car toutes les sous-classes en hériteront de manière appropriée).
  2. L'exception est explicitement levée par une implémentation - c'est la méthode recommandée pour empêcher la clonabilité dans une sous-classe lorsque la superclasse l'est Cloneable.

Ce dernier cas ne peut pas se produire dans votre classe (car vous appelez directement la méthode de la superclasse dans le trybloc, même si elle est invoquée depuis un appel de sous-classe super.clone()) et le premier ne devrait pas car votre classe devrait clairement implémenter Cloneable.

Fondamentalement, vous devez enregistrer l'erreur à coup sûr, mais dans ce cas particulier, cela ne se produira que si vous gâchez la définition de votre classe. Traitez-le donc comme une version vérifiée de NullPointerException(ou similaire) - il ne sera jamais lancé si votre code est fonctionnel.


Dans d'autres situations, vous devez être préparé à cette éventualité - il n'y a aucune garantie qu'un objet donné soit clonable, donc lorsque vous interceptez l'exception, vous devez prendre les mesures appropriées en fonction de cette condition (continuer avec l'objet existant, adopter une stratégie de clonage alternative par exemple sérialiser-désérialiser, lancer un IllegalParameterExceptionsi votre méthode nécessite le paramètre par clonable, etc. etc.).

Edit : Bien que dans l'ensemble, je devrais souligner que oui, il clone()est vraiment difficile à implémenter correctement et difficile pour les appelants de savoir si la valeur de retour sera ce qu'ils veulent, doublement quand on considère les clones profonds vs peu profonds. Il est souvent préférable d'éviter complètement tout cela et d'utiliser un autre mécanisme.

Andrzej Doyle
la source
Si un objet expose une méthode de clonage publique, tout objet dérivé qui ne la prend pas en charge violerait le principe de substitution de Liskov. Si une méthode de clonage est protégée, je pense qu'il serait préférable de la cacher avec autre chose qu'une méthode renvoyant le type approprié, pour empêcher une sous-classe d'essayer même d'appeler super.clone().
supercat du
5

Utilisez la sérialisation pour effectuer des copies complètes. Ce n'est pas la solution la plus rapide mais cela ne dépend pas du type.

Michał Ziober
la source
C'était une solution formidable et évidente étant donné que j'utilisais déjà GSON.
Jacob Tabak du
3

Vous pouvez implémenter des constructeurs de copie protégée comme ceci:

/* 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
la source
3
Cela ne fonctionne pas la plupart du temps. Vous ne pouvez pas appeler clone()l'objet renvoyé par à getMySecondMember()moins qu'il n'ait une public cloneméthode.
polygenelubricants
De toute évidence, vous devez implémenter ce modèle sur chaque objet que vous souhaitez cloner ou trouver un autre moyen de cloner en profondeur chaque membre.
Dolph
Oui, c'est une condition nécessaire.
polygenelubricants
1

Même si la plupart des réponses ici sont valables, je dois dire que votre solution est également la manière dont les développeurs d'API Java le font. (Soit Josh Bloch ou Neal Gafter)

Voici un extrait de 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);
    }
}

Comme vous l'avez remarqué et d'autres mentionnés, CloneNotSupportedExceptionn'a presque aucune chance d'être jeté si vous avez déclaré que vous implémentez l' Cloneableinterface.

En outre, vous n'avez pas besoin de remplacer la méthode si vous ne faites rien de nouveau dans la méthode remplacée. Vous ne devez le remplacer que lorsque vous devez effectuer des opérations supplémentaires sur l'objet ou que vous devez le rendre public.

En fin de compte, il est toujours préférable de l'éviter et de le faire d'une autre manière.

Haggra
la source
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
la source
Vous venez de l'écrire dans le système de fichiers et de relire l'objet. D'accord, est-ce la meilleure méthode pour gérer le clonage? Quelqu'un de la communauté SO peut-il commenter cette approche? Je suppose que cela lie inutilement le clonage et la sérialisation - deux concepts entièrement différents. J'attendrai de voir ce que les autres auront à dire à ce sujet.
Saurabh Patil
2
Ce n'est pas dans le système de fichiers, mais dans la mémoire principale (ByteArrayOutputStream). pour les objets profondément imbriqués, c'est la solution qui fonctionne bien. Surtout si vous n'en avez pas besoin souvent, par exemple en test unitaire. où la performance n'est pas l'objectif principal.
AlexWien
0

Ce n'est pas parce que l'implémentation java de Cloneable est cassée que vous ne pouvez pas en créer une.

Si le véritable objectif d'OP était de créer un clone profond, je pense qu'il est possible de créer une interface comme celle-ci:

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

puis utilisez le constructeur prototype mentionné précédemment pour l'implémenter:

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

et une autre classe avec un champ objet 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);
    }
}

De cette façon, vous pouvez facilement cloner en profondeur un objet de la classe BClass sans avoir besoin de @SuppressWarnings ou d'un autre code astucieux.

Francesco Rogo
la source