¿Cómo anular correctamente el método de clonación?

114

Necesito implementar un clon profundo en uno de mis objetos que no tiene superclase.

¿Cuál es la mejor manera de manejar el cheque CloneNotSupportedExceptionlanzado por la superclase (que es Object)?

Un compañero de trabajo me aconsejó que lo manejara de la siguiente manera:

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

    // Deep clone member fields here

    return foo;
}

Esto me parece una buena solución, pero quería lanzarlo a la comunidad de StackOverflow para ver si hay otras ideas que pueda incluir. ¡Gracias!

Cuga
fuente
6
Si sabe que la clase principal se implementa Cloneable, lanzar un en AssertionErrorlugar de un simple Errores un poco más expresivo.
Andrew Duffy
Editar: Prefiero no usar clone (), pero el proyecto ya está basado en él y no valdría la pena refactorizar todas las referencias en este punto.
Cuga
Es mejor morder la bala ahora que después (bueno, a menos que esté a punto de llegar a la producción).
Tom Hawtin - tackline
Nos queda un mes y medio en el cronograma hasta que esto se complete. No podemos justificar el tiempo.
Cuga
Creo que esta solución es perfecta. Y no se sienta mal por usar clon. Si tiene una clase que se usa como un objeto de transporte y contiene solo primitves e inmutables como String y Enums, todo lo demás que no sea clonar sería una completa pérdida de tiempo por razones dogmáticas. ¡Solo tenga en cuenta lo que hace el clon y lo que no! (sin clonación profunda)
TomWolk

Respuestas:

125

¿Absolutamente tienes que usar clone? La mayoría de la gente está de acuerdo en que Java no funciona clone.

Josh Bloch sobre diseño: constructor de copias versus clonación

Si ha leído el artículo sobre la clonación en mi libro, especialmente si lee entre líneas, sabrá que creo que cloneestá profundamente roto. [...] Es una pena que Cloneablese rompa, pero pasa.

Puede leer más discusiones sobre el tema en su libro Effective Java 2nd Edition, Item 11: Override clonejuiciosamente . En cambio, recomienda utilizar un constructor de copias o una fábrica de copias.

Continuó escribiendo páginas de páginas sobre cómo, si cree que debe hacerlo, debe implementarlo clone. Pero cerró con esto:

¿Son todas estas complejidades realmente necesarias? Raramente. Si extiende una clase que implementa Cloneable, no tiene más remedio que implementar un clonemétodo que se comporte bien . De lo contrario, es mejor que proporcione medios alternativos de copia de objetos o simplemente no brinde la capacidad .

El énfasis era suyo, no mío.


Como dejó en claro que no tiene más remedio que implementar clone, esto es lo que puede hacer en este caso: asegúrese de eso MyObject extends java.lang.Object implements java.lang.Cloneable. Si ese es el caso, puede garantizar que NUNCA contraerá un CloneNotSupportedException. Lanzar AssertionErrorcomo algunos han sugerido parece razonable, pero también puede agregar un comentario que explique por qué nunca se ingresará el bloque de captura en este caso particular .


Alternativamente, como también han sugerido otros, quizás pueda implementar clonesin llamar super.clone.

poligenelubricantes
fuente
1
Desafortunadamente, el proyecto ya está escrito usando el método de clonación, de lo contrario, no lo usaría en absoluto. Estoy totalmente de acuerdo con usted en que la implementación de clon de Java es fakakta.
Cuga
5
Si una clase y todas sus superclases llaman super.clone()dentro de sus métodos de clonación, una subclase generalmente solo tendrá que anular clone()si agrega nuevos campos cuyo contenido necesitaría ser clonado. Si alguna superclase usa en newlugar de super.clone(), entonces todas las subclases deben anular clone()si agregan o no campos nuevos.
supercat
56

A veces es más sencillo implementar un constructor de copias:

public MyObject (MyObject toClone) {
}

Le ahorra la molestia de manipular CloneNotSupportedException, trabaja con finalcampos y no tiene que preocuparse por el tipo para devolver.

Aaron Digulla
fuente
11

La forma en que funciona su código es bastante similar a la forma "canónica" de escribirlo. Sin AssertionErrorembargo, lanzaría una dentro de la captura. Señala que nunca se debe alcanzar esa línea.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Chris Jester-Young
fuente
Vea la respuesta de @polygenelubricants para saber por qué esto podría ser una mala idea.
Karl Richter
@KarlRichter He leído su respuesta. No dice que sea una mala idea, aparte de que Cloneableen general es una idea rota, como se explica en Effective Java. El OP ya expresó que tienen que usar Cloneable. Así que no tengo idea de qué otra manera podría mejorarse mi respuesta, excepto quizás eliminarla por completo.
Chris Jester-Young
9

Hay dos casos en los que se CloneNotSupportedExceptionlanzará:

  1. La clase que se está Cloneableclonando no se implementa (asumiendo que la clonación real eventualmente difiere del Objectmétodo de clonación). Si la clase en la que está escribiendo este método lo implementa Cloneable, esto nunca sucederá (ya que cualquier subclasa lo heredará de manera apropiada).
  2. La excepción es lanzada explícitamente por una implementación; esta es la forma recomendada de evitar la clonabilidad en una subclase cuando la superclase lo es Cloneable.

El último caso no puede ocurrir en su clase (ya que está llamando directamente al método de la superclase en el trybloque, incluso si se invoca desde una llamada de subclase super.clone()) y el primero no debería, ya que su clase claramente debería implementar Cloneable.

Básicamente, debes registrar el error con seguridad, pero en este caso en particular, solo sucederá si arruinas la definición de tu clase. Por lo tanto, trátelo como una versión verificada de NullPointerException(o similar): nunca se lanzará si su código es funcional.


En otras situaciones, necesitaría estar preparado para esta eventualidad; no hay garantía de que un objeto dado sea clonable, por lo que al detectar la excepción, debe tomar las medidas adecuadas según esta condición (continuar con el objeto existente, tomar una estrategia de clonación alternativa por ejemplo, serializar-deserializar, lanzar un IllegalParameterExceptionsi su método requiere el parámetro por clonable, etc., etc.).

Editar : Aunque en general debo señalar que sí, clone()realmente es difícil de implementar correctamente y es difícil para las personas que llaman saber si el valor de retorno será el que quieren, doblemente cuando se consideran clones profundos vs superficiales. A menudo es mejor evitar todo el asunto por completo y usar otro mecanismo.

Andrzej Doyle
fuente
Si un objeto expone un método de clonación público, cualquier objeto derivado que no lo admita violaría el principio de sustitución de Liskov. Si un método de clonación está protegido, creo que sería mejor sombrearlo con algo que no sea un método que devuelva el tipo adecuado, para evitar que una subclase siquiera intente llamar super.clone().
supercat
5

Utilice la serialización para realizar copias en profundidad. Esta no es la solución más rápida, pero no depende del tipo.

Michał Ziober
fuente
Esta fue una gran y obvia solución considerando que ya estaba usando GSON.
Jacob Tabak
3

Puede implementar constructores de copia protegidos así:

/* 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
fuente
3
Esto no funciona la mayor parte del tiempo. No puede llamar clone()al objeto devuelto por a getMySecondMember()menos que tenga un public clonemétodo.
polygenelubricants
Obviamente, debe implementar este patrón en cada objeto que desee poder clonar o encontrar otra forma de clonar en profundidad cada miembro.
Dolph
Sí, ese es un requisito necesario.
polygenelubricants
1

Por mucho que la mayoría de las respuestas aquí sean válidas, debo decir que su solución también es cómo lo hacen los desarrolladores de la API de Java. (Ya sea Josh Bloch o Neal Gafter)

Aquí hay un extracto de openJDK, clase 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);
    }
}

Como habrás notado y otros mencionaron, CloneNotSupportedExceptioncasi no tiene posibilidad de ser lanzado si declaras que implementas la Cloneableinterfaz.

Además, no es necesario que anule el método si no hace nada nuevo en el método anulado. Solo necesita anularlo cuando necesite realizar operaciones adicionales en el objeto o necesite hacerlo público.

En última instancia, es mejor evitarlo y hacerlo de otra manera.

Haggra
fuente
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
fuente
Así que lo acaba de escribir en el sistema de archivos y ha leído el objeto. Bien, ¿es este el mejor método para manejar la clonación? ¿Alguien de la comunidad SO puede comentar sobre este enfoque? Supongo que esto vincula innecesariamente la clonación y la serialización, dos conceptos completamente diferentes. Esperaré a ver qué tienen que decir otros sobre esto.
Saurabh Patil
2
No está en el sistema de archivos, está en la memoria principal (ByteArrayOutputStream). para objetos profundamente anidados, es la solución que funciona bien. Especialmente si no lo necesita con frecuencia, por ejemplo, en la prueba unitaria. donde el rendimiento no es el objetivo principal.
AlexWien
0

El hecho de que la implementación de Java de Cloneable esté rota no significa que no pueda crear una propia.

Si el propósito real de OP era crear un clon profundo, creo que es posible crear una interfaz como esta:

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

luego use el constructor de prototipos mencionado antes para 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);
    }
}

y otra clase con un campo de objeto 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 esta manera, puede clonar fácilmente en profundidad un objeto de la clase BClass sin necesidad de @SuppressWarnings u otro código engañoso.

Francesco Rogo
fuente