web-dev-qa-db-fra.com

Comment remplacer correctement la méthode de clonage?

Je dois implémenter un clone profond dans l'un de mes objets sans super-classe.

Quelle est la meilleure façon de gérer la CloneNotSupportedException vérifiée levée 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 semble être une bonne solution pour moi, mais je voulais l’envoyer à la communauté StackOverflow pour savoir si je peux inclure d’autres informations. Merci!

105
Cuga

Devez-vous absolument utiliser clone? La plupart des gens s'accordent pour dire que la clone de Java est cassée.

Josh Bloch sur Design - Copy Constructor versus Cloning

Si vous avez lu l'article sur le clonage dans mon livre, surtout si vous lisez entre les lignes, vous saurez que je pense que clone est profondément brisé. [...] C'est dommage que Cloneable soit cassé, mais cela arrive.

Vous pouvez lire plus de discussion sur le sujet dans son livre Effective Java 2nd Edition, Point 11: Remplacer clone judicieusement. Il recommande plutôt d'utiliser un constructeur de copie ou une fabrique de copie.

Il a ensuite écrit des pages de pages sur la manière dont vous devriez, si vous en ressentiez le besoin, mettre en œuvre clone. Mais il a terminé avec ceci:

Toute cette complexité est-elle vraiment nécessaire? Rarement. Si vous étendez une classe qui implémente Cloneable, vous n’avez guère le choix que d’implémenter une méthode clone bien comportée. Sinon, il est préférable de fournir un moyen alternatif de copier un objet, ou simplement de ne pas fournir la capacité}.

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


Puisque vous avez clairement indiqué que vous n’aviez que le choix d’implémenter 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 alors garantir que vous _ NEVER attraperez une CloneNotSupportedException. Lancer AssertionError comme certains l'ont suggéré semble raisonnable, mais vous pouvez aussi 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 éventuellement implémenter clone sans appeler super.clone.

117
polygenelubricants

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

public MyObject (MyObject toClone) {
}

Cela vous évite de manipuler CloneNotSupportedException, fonctionne avec les champs final et vous n'avez pas à vous soucier du type à renvoyer.

53
Aaron Digulla

La façon dont votre code fonctionne est assez proche de la façon "canonique" de l'écrire. Je jetterais cependant une AssertionError dans la capture. Il signale que cette ligne ne doit jamais être atteinte.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
11
Chris Jester-Young

Il y a deux cas dans lesquels la CloneNotSupportedException sera lancée:

  1. La classe en cours de clonage n'implémente pas Cloneable (en supposant que le clonage réel finit par passer à la méthode de clonage de Object). Si la classe dans laquelle vous écrivez cette méthode implémente Cloneable, cela n'arrivera jamais (puisque 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 le moyen recommandé d'empêcher la clonabilité dans une sous-classe lorsque la superclasse est Cloneable.

Ce dernier cas ne peut pas se produire dans votre classe (puisque vous appelez directement la méthode de la superclasse dans le bloc try, même s'il est appelé depuis une sous-classe appelant super.clone()) et le premier ne devrait pas, car votre classe devrait clairement implémenter Cloneable.

Fondamentalement, vous devez enregistrer l'erreur avec certitude, mais dans ce cas particulier, cela ne se produira que si vous bousillez la définition de votre classe. Ainsi, traitez-le comme une version vérifiée de NullPointerException (ou similaire) - il ne sera jamais jeté si votre code est fonctionnel.


Dans d’autres situations, vous devez vous préparer à cette éventualité - il n’existe aucune garantie qu’un objet donné est clonable. Par conséquent, lors de la capture de l’exception, vous devez prendre les mesures appropriées en fonction de cette condition (continuez avec l’objet existant, prenez une stratégie de clonage alternative, par exemple sérialiser-désérialiser, lancer une IllegalParameterException si votre méthode requiert le paramètre par cloneable, etc. etc.).

Edit : Bien que dans l’ensemble, je tiens à préciser que, oui, clone() est vraiment difficile à mettre en œuvre correctement et qu'il est difficile pour les appelants de savoir si la valeur de retour sera celle qu'ils veulent, ce qui est d'autant plus vrai si vous envisagez des clones profonds ou peu profonds. Il vaut souvent mieux éviter tout le problème et utiliser un autre mécanisme.

9
Andrzej Doyle

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

5
Michał Ziober

Vous pouvez implémenter des constructeurs de copie protégés 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);
}
3
Dolph
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) {

                }
        }
    }
}
0
Kvant_hv

Ce n’est pas parce que l’implémentation de Clonable par Java est cassée que cela ne signifie pas que vous ne pouvez pas créer le vôtre.

Si l'objectif réel de l'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 de 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 d'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 manière, vous pouvez facilement cloner en profondeur un objet de classe BClass sans avoir besoin de @SuppressWarnings ou d'un autre code fantaisiste.

0
Francesco Rogo

Même si la plupart des réponses ici sont valables, je dois dire que votre solution tient également compte de la façon 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 mentionné par d'autres personnes, CloneNotSupportedException n'a presque aucune chance d'être lancé si vous déclarez implémenter l'interface Cloneable.

En outre, il n'est pas nécessaire de remplacer la méthode si vous ne faites rien de nouveau dans la méthode remplacée. Vous devez uniquement le remplacer lorsque vous devez effectuer des opérations supplémentaires sur l'objet ou si 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.

0
Haggra