clone () vs constructor de copia vs método de fábrica?

81

Hice una búsqueda rápida en Google sobre la implementación de clone () en Java y encontré: http://www.javapractices.com/topic/TopicAction.do?Id=71

Tiene el siguiente comentario:

Los constructores de copia y los métodos de fábrica estáticos proporcionan una alternativa a la clonación y son mucho más fáciles de implementar.

Todo lo que quiero hacer es hacer una copia en profundidad. Implementar clone () parece tener mucho sentido, pero este artículo altamente clasificado en Google me da un poco de miedo.

Estos son los problemas que noté:

Los constructores de copia no funcionan con genéricos.

Aquí hay un pseudocódigo que no se compilará.

public class MyClass<T>{
   ..
   public void copyData(T data){
       T copy=new T(data);//This isn't going to work.    
   }
   ..
}

Ejemplo 1: uso de un constructor de copia en una clase genérica.

Los métodos de fábrica no tienen nombres estándar.

Es muy bueno tener una interfaz para código reutilizable.

public class MyClass<T>{
    ..
    public void copyData(T data){
        T copy=data.clone();//Throws an exception if the input was not cloneable
    }
    ..
}

Ejemplo 2: uso de clone () en una clase genérica.

Me di cuenta de que la clonación no es un método estático, pero ¿no sería necesario realizar copias profundas de todos los campos protegidos? Al implementar clone (), el esfuerzo adicional para lanzar excepciones en subclases no clonables me parece trivial.

¿Me estoy perdiendo de algo? Se agradecería cualquier información.

Usuario1
fuente

Respuestas:

52

Básicamente, el clon está roto . Nada funcionará fácilmente con genéricos. Si tiene algo como esto (abreviado para expresar el punto):

public class SomeClass<T extends Copyable> {


    public T copy(T object) {
        return (T) object.copy();
    }
}

interface Copyable {
    Copyable copy();
}

Luego, con una advertencia del compilador, puede hacer el trabajo. Debido a que los genéricos se borran en tiempo de ejecución, algo que haga una copia tendrá una advertencia del compilador que generará una conversión. No es evitable en este caso. . Es evitable en algunos casos (gracias, kb304) pero no en todos. Considere el caso en el que tiene que admitir una subclase o una clase desconocida que implementa la interfaz (como si estuviera iterando a través de una colección de copiables que no necesariamente generaban la misma clase).

Yishai
fuente
2
También es posible hacerlo sin la advertencia del compilador.
akarnokd
@ kd304, si quiere decir que suprime la advertencia del compilador, es cierto, pero en realidad no es diferente. Si quiere decir que puede evitar la necesidad de una advertencia por completo, por favor explique.
Yishai
@Yishai: Mira mi respuesta. Y no me refería a la advertencia de supresión.
akarnokd
Votar a la baja porque Copyable<T>en la respuesta de kd304 tiene mucho más sentido de usar.
Simon Forsberg
25

También está el patrón Builder. Consulte Effective Java para obtener más detalles.

No entiendo tu evaluación. En un constructor de copias, conoce perfectamente el tipo, ¿por qué es necesario utilizar genéricos?

public class C {
   public int value;
   public C() { }
   public C(C other) {
     value = other.value;
   }
}

Recientemente hubo una pregunta similar aquí .

public class G<T> {
   public T value;
   public G() { }
   public G(G<? extends T> other) {
     value = other.value;
   }
}

Una muestra ejecutable:

public class GenTest {
    public interface Copyable<T> {
        T copy();
    }
    public static <T extends Copyable<T>> T copy(T object) {
        return object.copy();
    }
    public static class G<T> implements Copyable<G<T>> {
        public T value;
        public G() {
        }
        public G(G<? extends T> other) {
            value = other.value;
        }
        @Override
        public G<T> copy() {
            return new G<T>(this);
        }
    }
    public static void main(String[] args) {
        G<Integer> g = new G<Integer>();
        g.value = 1;
        G<Integer> f = g.copy();
        g.value = 2;
        G<Integer> h = copy(g);
        g.value = 3;
        System.out.printf("f: %s%n", f.value);
        System.out.printf("g: %s%n", g.value);
        System.out.printf("h: %s%n", h.value);
    }
}
akarnokd
fuente
1
+1 esta es la forma más simple pero efectiva de implementar un constructor de copias
dfa
Tenga en cuenta que MyClass anterior es un genérico, StackOverflow se tragó el <T>.
Usuario1
Se corrigió el formato de su pregunta. Utilice cuatro espacios en cada línea en lugar de etiquetas pre + código.
akarnokd
Recibo un error en el método de copia de G - "debe anular un método de
superclase
3
(Sé que esto es antiguo, pero para la posteridad) @CarlPritchett: este error aparecerá en Java 1.5 y más abajo. Se permite marcar métodos de interfaz como @ Override a partir de Java 1.6.
Carrotman42
10

Java no tiene constructores de copia en el mismo sentido que C ++.

Puede tener un constructor que tome un objeto del mismo tipo como argumento, pero pocas clases lo admiten. (menos que el número que admite la clonación)

Para un clon genérico, tengo un método auxiliar que crea una nueva instancia de una clase y copia los campos del original (una copia superficial) usando reflejos (en realidad, algo así como reflejos pero más rápido)

Para una copia profunda, un enfoque simple es serializar el objeto y deserializarlo.

Por cierto: Mi sugerencia es usar objetos inmutables, entonces no necesitará clonarlos. ;)

Peter Lawrey
fuente
¿Podría explicar por qué no necesitaría la clonación si usara objetos inmutables? En mi aplicación, necesito tener otro objeto que tenga exactamente los mismos datos que el objeto existente. Así que necesito copiarlo de alguna manera
Zhenya
2
@Ievgen si necesita dos referencias a los mismos datos, puede copiar la referencia. Sólo tiene que copiar el contenido de un objeto si se podría cambiar (pero para los objetos inmutables usted sabe que no lo hará)
Peter Lawrey
Probablemente no entiendo los beneficios de los objetos inmutables. Supongamos que tengo una entidad JPA / hibernate, y necesito crear otra entidad JPA basada en la existente, pero necesito cambiar la identificación de la nueva entidad. ¿Cómo lo haría con objetos inmutables?
Zhenya
@Ievgen, por definición, no puede cambiar objetos inmutables. Stringpor ejemplo, es un objeto inmutable y puede pasar un String sin tener que copiarlo.
Peter Lawrey
6

Creo que la respuesta de Yishai podría mejorarse, por lo que no podemos tener ninguna advertencia con el siguiente código:

public class SomeClass<T extends Copyable<T>> {

    public T copy(T object) {
        return object.copy();
    }
}

interface Copyable<T> {
    T copy();
}

De esta manera, una clase que necesita implementar una interfaz copiable tiene que ser así:

public class MyClass implements Copyable<MyClass> {

    @Override
    public MyClass copy() {
        // copy implementation
        ...
    }

}
Alepac
fuente
2
Casi. La interfaz copiable debe declararse como:, interface Copyable<T extends Copyable>que es lo más parecido a un tipo propio que puede codificar en Java.
Recurse
Por supuesto que quisiste decir interface Copyable<T extends Copyable<T>>, ¿verdad? ;)
Adowrath
3

A continuación se muestran algunas desventajas debido a las cuales muchos desarrolladores no usan Object.clone()

  1. El uso de Object.clone()método requiere que agreguemos mucha sintaxis a nuestro código, como implementar Cloneableinterfaz, definir clone()método y manejar CloneNotSupportedExceptiony finalmente llamar Object.clone()y convertirlo en nuestro objeto.
  2. Cloneablela interfaz carece de clone()método, es una interfaz de marcador y no tiene ningún método, y aún así necesitamos implementarla solo para decirle a JVM que podemos realizar clone()en nuestro objeto.
  3. Object.clone()está protegido por lo que tenemos que proporcionar el nuestro clone()y llamar indirectamente Object.clone()desde él.
  4. No tenemos ningún control sobre la construcción de objetos porque Object.clone()no invoca ningún constructor.
  5. Si estamos escribiendo un clone()método en una clase secundaria, por ejemplo Person, todas sus superclases deberían definir el clone()método en ellas o heredarlo de otra clase principal, de lo contrario la super.clone()cadena fallará.
  6. Object.clone()Admite solo copia superficial, por lo que los campos de referencia de nuestro objeto recién clonado aún contendrán objetos cuyos campos de nuestro objeto original contenían. Para superar esto, necesitamos implementar clone()en cada clase que hace referencia a nuestra clase y luego clonarlos por separado en nuestro clone()método como en el siguiente ejemplo.
  7. No podemos manipular los campos finales en Object.clone()porque los campos finales solo se pueden cambiar a través de constructores. En nuestro caso, si queremos que cada Personobjeto sea único por id, obtendremos el objeto duplicado si lo usamos Object.clone()porque Object.clone()no llamará al constructor y idno se puede modificar el campo final Person.clone().

Los constructores de copias son mejores que Object.clone()porque

  1. No nos obligue a implementar ninguna interfaz ni lanzar ninguna excepción, pero seguramente podemos hacerlo si es necesario.
  2. No necesita yesos.
  3. No nos exija que dependamos de un mecanismo de creación de objetos desconocido.
  4. No requiera que la clase de los padres siga ningún contrato o implemente nada.
  5. Permítanos modificar los campos finales.
  6. Permítanos tener un control completo sobre la creación de objetos, podemos escribir nuestra lógica de inicialización en él.

Más información sobre Clonación de Java: constructor de copias frente a clonación

Naresh Joshi
fuente
1

Por lo general, clone () funciona en conjunto con un constructor de copia protegido. Esto se hace porque clone (), a diferencia de un constructor, puede ser virtual.

En un cuerpo de clase para Derivado de una superclase Base tenemos

class Derived extends Base {
}

Entonces, en su forma más simple, agregaría a esto un constructor de copia virtual con el clon (). (En C ++, Joshi recomienda clonar como constructor de copia virtual).

protected Derived() {
    super();
}

protected Object clone() throws CloneNotSupportedException {
    return new Derived();
}

Se vuelve más complicado si desea llamar a super.clone () como se recomienda y tiene que agregar estos miembros a la clase, puede intentar esto

final String name;
Address address;

/// This protected copy constructor - only constructs the object from super-class and
/// sets the final in the object for the derived class.
protected Derived(Base base, String name) {
   super(base);
   this.name = name;
}

protected Object clone() throws CloneNotSupportedException {
    Derived that = new Derived(super.clone(), this.name);
    that.address = (Address) this.address.clone();
}

Ahora, si es una ejecución, tienes

Base base = (Base) new Derived("name");

y luego lo hiciste

Base clone = (Base) base.clone();

esto invocaría, clone () en la clase Derived (la de arriba), esto invocaría super.clone (), que puede o no estar implementado, pero se recomienda llamarlo. La implementación luego pasa la salida de super.clone () a un constructor de copia protegido que toma una Base y le pasa los miembros finales.

Ese constructor de copia luego invoca al constructor de copia de la superclase (si sabe que tiene uno) y establece las finales.

Cuando regresa al método clone (), establece los miembros no finales.

Los lectores astutos notarán que si tiene un constructor de copia en la Base, será llamado por super.clone () - y será llamado nuevamente cuando invoque al superconstructor en el constructor protegido, por lo que puede estar llamando al super constructor de copias dos veces. Con suerte, si está bloqueando recursos, lo sabrá.

Bonaparte
fuente
0

Un patrón que puede funcionar para usted es la copia a nivel de bean. Básicamente, usa un constructor sin argumentos y llama a varios establecedores para proporcionar los datos. Incluso puede utilizar las distintas bibliotecas de propiedades de los beans para establecer las propiedades con relativa facilidad. Esto no es lo mismo que hacer un clon () pero para muchos propósitos prácticos está bien.

Sr. Brillante y Nuevo 安 宇
fuente
0

La interfaz Cloneable está rota, en el sentido de que es inútil pero la clonación funciona bien y puede conducir a un mejor rendimiento para objetos grandes (8 campos y más, pero luego fallará el análisis de escape). tan preferible usar el constructor de copia la mayor parte del tiempo. El uso de clonar en una matriz es más rápido que Arrays.copyOf porque se garantiza que la longitud será la misma.

más detalles aquí https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html

usuario3996996
fuente
0

Si uno no es 100% consciente de todas las peculiaridades de clone(), le aconsejo que se mantenga alejado. Yo no diría que está clone()roto. Yo diría: úselo solo cuando esté completamente seguro de que es su mejor opción. Un constructor de copias (o un método de fábrica, creo que eso realmente no importa) es fácil de escribir (tal vez largo, pero fácil), solo copia lo que quieres que se copie y copia de la forma en que quieres que se copien las cosas. Puede recortarlo según sus necesidades exactas.

Además: es fácil depurar lo que sucede cuando llama a su constructor de copia / método de fábrica.

Y clone()no crea una copia "profunda" de su objeto fuera de la caja, asumiendo que quiere decir que no solo Collectionse copian las referencias (por ejemplo, a ). Pero lea más sobre lo profundo y lo superficial aquí: copia profunda, copia superficial, clonación

perdonmissjackson
fuente
-2

Lo que le falta es que la clonación crea copias superficiales por defecto y por convención, y que hacer que cree copias profundas no es, en general, factible.

El problema es que realmente no se pueden crear copias profundas de gráficos de objetos cíclicos sin poder realizar un seguimiento de los objetos que se han visitado. clone () no proporciona tal seguimiento (ya que tendría que ser un parámetro para .clone ()) y, por lo tanto, solo crea copias superficiales.

Incluso si su propio objeto invoca .clone para todos sus miembros, aún no será una copia profunda.

Martin contra Löwis
fuente
4
Puede que no sea factible hacer una copia profunda de clone () para cualquier objeto arbitrario, pero en la práctica esto es manejable para muchas jerarquías de objetos. Solo depende de qué tipo de objetos tenga y cuáles son sus variables miembro.
Mr. Shiny and New 安 宇
Hay mucha menos ambigüedad de lo que muchas personas afirman. Si uno tiene un clonable SuperDuperList<T>o un derivado del mismo, entonces la clonación debería producir una nueva instancia del mismo tipo que el que se clonó; debe estar separado del original, pero debe hacer referencia a los mismos T, en el mismo orden que el original. Nada de lo que se haga en ninguna de las listas a partir de ese momento debería afectar las identidades de los objetos almacenados en la otra. No conozco ninguna "convención" útil que requiera una colección genérica para exhibir cualquier otro comportamiento.
supercat