Métodos de objetos de Java: clone ()

     

    Introducción

    Este artículo es una continuación de una serie de artículos que describen los métodos a menudo olvidados de la clase Object base del lenguaje Java. Los siguientes son los métodos del objeto Java base que están presentes en todos los objetos Java debido a la herencia implícita de Object.

    • Encadenar
    • a clase
    • es igual a
    • c√≥digo hash
    • clon (est√°s aqu√≠)
    • finalizar
    • esperar y notificar

    El enfoque de este art√≠culo es el clone() m√©todo que se utiliza para generar copias claramente separadas (nuevas instancias) de un objeto. Tambi√©n debo se√Īalar que el clone() El m√©todo es probablemente uno de los m√©todos m√°s controvertidos disponibles en la clase Object debido a algunos comportamientos extra√Īos y caracter√≠sticas de implementaci√≥n.

    Por qué existe la necesidad de clonar () un objeto

    Primero me gustaría comenzar con por qué puede ser necesario crear un clon o copia de un objeto en primer lugar. Volveré a utilizar mi clase Person de artículos anteriores de esta serie para demostraciones, de particular importancia es que esta es una versión mutable de la misma; de lo contrario, la copia sería un punto discutible.

    El código se muestra a continuación:

    import java.time.LocalDate;
    
    public class Person {
        private String firstName;
        private String lastName;
        private LocalDate dob;
    
        public Person(String firstName, String lastName, LocalDate dob) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dob = dob;
        }
    
        public String getFirstName() { return firstName; }
        public void setFirstName(String firstName) { this.firstName = firstName; }
    
        public String getLastName() { return lastName; }
        public void setLastName(String lastName) { this.lastName = lastName; }
    
    
        public LocalDate getDob() { return dob; }
        public void setDob(LocalDate dob) { this.dob = dob; }
    
        @Override
        public String toString() {
            return "<Person: firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">";
        }
    
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((dob == null) ? 0 : dob.hashCode());
            result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
            result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
            return result;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof Person)) {
                return false;
            }
            Person p = (Person)o;
            return firstName.equals(p.firstName)
                    && lastName.equals(p.lastName)
                    && dob.equals(p.dob);
        }
    }
    

    Empiezo mi discusión creando un par de variables enteras x y y junto con una instancia de Person y asígnela a una variable llamada me. Entonces asigno me a otra variable llamada me2 que luego cambio el firstName campo en me2 y mostrar el contenido de ambas variables, así:

    import java.time.LocalDate;
    
    public class Main {
        public static void main(String[] args) {
            int x = 10;
            int y = x;
            y = 20;
            System.out.println("x = " + x);
            System.out.println("y = " + y);
    
            Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
            Person me2 = me;
            me2.setFirstName("Joe");
            System.out.println("me = " + me);
            System.out.println("me2 = " + me2);
        }
    }
    

    Salida:

    x = 10
    y = 20
    me = <Person: firstName=Joe, lastName=McQuistan, dob=1987-09-23>
    me2 = <Person: firstName=Joe, lastName=McQuistan, dob=1987-09-23>
    

    Ahora hay una buena posibilidad de que muchos de ustedes hayan captado ese peque√Īo error … pero, para que todos est√©n en el mismo nivel de comprensi√≥n, perm√≠tanme explicarles lo que acaba de pasar all√≠. En Java, tiene dos categor√≠as principales de tipos de datos: tipos de valor (alias, primitivas) y tipos de referencia (alias, objetos). En mi ejemplo anterior, los objetos Person como me y me2 son del tipo de referencia del objeto Persona. A diferencia de los tipos de referencia Persona x y y son tipos de valor de primitivas int.

    Como se acaba de hacer evidente, las asignaciones con tipos de referencia se tratan de manera diferente a decir un n√ļmero entero, o quiz√°s m√°s exactamente como int en el lenguaje Java. Cuando asigna una variable de referencia a otra variable de referencia, simplemente le est√° diciendo la ubicaci√≥n donde se puede hacer referencia a ese objeto en la memoria, que es muy diferente a la copia real de contenido que ocurre cuando hace lo mismo con los tipos de valor.

    Por eso cuando cambié el valor de la me2 variable de referencia firstName campo También vi el mismo cambio en el me variable de referencia, estaban haciendo referencia al mismo objeto en la memoria. Por estas razones, es importante poder crear copias reales (clones) de objetos de referencia y, por lo tanto, la necesidad de clone() método.

    Cómo clonar () un objeto

    Como mencion√© anteriormente, clone() El m√©todo de la clase Object es un poco controvertido en la comunidad de programaci√≥n Java. La raz√≥n de esto es que para implementar el clone() m√©todo que necesita para implementar una interfaz peculiar llamada Cloneable del paquete “java.lang” que proporciona a su clase la capacidad de exponer un clone() m√©todo. Esto es necesario porque el clone() El m√©todo en la clase Object est√° protegido y, por lo tanto, no es accesible desde el c√≥digo del cliente que trabaja con su clase. Adem√°s, el comportamiento de la creaci√≥n de objetos es bastante inusual ya que la instancia se crea sin invocar la codiciada new operador que deja a muchos, incluido yo mismo, un poco inc√≥modos.

    Sin embargo, para completar, describiré una forma válida de implementar un clone() método al implementar el Cloneable interfaz, pero también terminaré con algunos mecanismos alternativos para crear nuevas instancias de objetos de una manera más idiomática de Java-esk.

    Ok, sin m√°s bromas, proceder√© a explicar c√≥mo clonar objetos a trav√©s de clone() dentro de mi clase Person. Primero implementar√© el Cloneable interfaz y agregue la anulaci√≥n p√ļblica clone() m√©todo que devuelve una instancia de tipo Object.

    Para una clase simple como Person que no contiene ning√ļn campo mutable, todo lo que se requiere es crear un clon es devolver una llamada al m√©todo de clonaci√≥n de Object de la clase base, as√≠:

    public class Person implements Cloneable {
        private String firstName;
        private String lastName;
        private LocalDate dob;
    
        public Person(String firstName, String lastName, LocalDate dob) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dob = dob;
        }
    
        // omitting other sections for brevity
    
        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    En este ejemplo, crear un clon de Persona es bastante simple y se logra así:

    public class Main {
        public static void main(String[] args) {
            Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
            Person me2 = null;
            try {
                me2 = (Person) me.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            me2.setFirstName("Joe");
            System.out.println("me = " + me);
            System.out.println("me2 = " + me2);
        }
    }
    

    Salida:

    me = <Person: firstName=Adam, lastName=McQuistan, dob=1987-09-23>
    me2 = <Person: firstName=Joe, lastName=McQuistan, dob=1987-09-23>
    

    An voil√† a me se hace el clon. Ahora, cuando actualizo el firstName propiedad de me2 utilizando el ejemplo anterior, el campo no se modifica en el me objeto. Aseg√ļrese de tener en cuenta la conversi√≥n expl√≠cita del clon devuelto del tipo Objeto al tipo Persona, que es necesario porque la interfaz necesita devolver una referencia del tipo Objeto.

    Desafortunadamente, aunque esta implementación del clone() El método solo funcionará en valores simples que contengan objetos que no tengan propiedades de referencia mutables. Si tuviera que agregar un par de campos mutables como mother de tipo Person y un family gama de Person objetos que necesitaría hacer algunos cambios para permitir que se realice una clonación segura.

    Para demostrar esto, necesito actualizar mi Person clase como tal.

    public class Person implements Cloneable {
        private String firstName;
        private String lastName;
        private LocalDate dob;
        private Person mother;
        private Person[] family;
    
        public Person(String firstName, String lastName, LocalDate dob) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dob = dob;
        }
    
        // omitting other methods for brevity
    
        public Person getMother() { return mother; }
        public void setMother(Person mother) { this.mother = mother; }
    
        public Person[] getFamily() { return family; }
        public void setFamily(Person[] family) { this.family = family; }
    
        @Override
        public Object clone() throws CloneNotSupportedException {
            Person personClone = (Person) super.clone();
            Person motherClone = (Person) mother.clone();
            Person[] familyClone = family.clone();
            personClone.setMother(motherClone);
            personClone.setFamily(familyClone);
            return personClone;
        }
    }
    

    Para garantizar que el objeto clonado tenga sus propias copias √ļnicas de los campos mutables del objeto original, mother y family, Debo hacer copias expl√≠citamente de ellos a trav√©s de clone() u otras formas como instanciar y establecer los valores a trav√©s del nuevo operador.

    Si no me tomara el tiempo específicamente para hacer clones individualmente de estos campos mutables, entonces los dos objetos Persona resultantes estarían haciendo referencia a lo mismo mother y family instancias de objetos mutables que serían un desastre terrible de depurar en el futuro. Esta copia explícita campo por campo de miembros de objetos mutables se conoce como copia profunda.

    Técnicas alternativas para crear copias de instancia

    Hay algunas otras formas de crear clones de objetos que he visto que usan t√©cnicas como serializaci√≥n, constructores de copias y m√©todos de f√°brica que crean copias de objetos. Sin embargo, en esta secci√≥n solo cubrir√© los dos √ļltimos porque personalmente no me importa mucho el uso de la serializaci√≥n para crear copias de objetos.

    Para empezar, cubriré el método del constructor de copias. Esta ruta de crear copias de objetos usando un constructor se basa en una firma que contiene solo un parámetro de su propio tipo que representa el objeto que se va a copiar, como public Person(Person p).

    Dentro del cuerpo del constructor de copia, a cada campo del objeto que se va a copiar se le asigna directamente una nueva instancia de esa clase en el caso de tipos de valor o se utiliza para crear nuevas instancias de sus campos en el caso de tipos de referencia.

    A continuación se muestra un ejemplo del uso de un constructor de copia para el Person clase:

    public class Person implements Cloneable {
        private String firstName;
        private String lastName;
        private LocalDate dob;
        private Person mother;
        private Person[] family;
    
        public Person(String firstName, String lastName, LocalDate dob) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dob = dob;
        }
    
        public Person(Person p) {
            this.firstName = new String(p.firstName);
            this.lastName = new String(p.lastName);
            this.dob = LocalDate.of(p.dob.getYear(),
                    p.dob.getMonth(),
                    p.dob.getDayOfMonth());
            if (p.mother != null) {
                this.mother = new Person(p.mother);
            }
            if (p.family != null) {
                this.family = new Person[p.family.length];
                for (int i = 0; i < p.family.length; i++) {
                    if (p.family[i] != null) {
                        this.family[i] = new Person(p.family[i]);
                    }
                }
            }
        }
    
        // omitting other methods for brevity
    
    }
    

    La otra técnica que mostraré utiliza un método de fábrica. La técnica del método de fábrica es esencialmente la misma que la de un constructor de copia, excepto que la nueva copia se crea dentro de un método de fábrica estático que devuelve una nueva instancia como copia, así:

    public class Person implements Cloneable {
        private String firstName;
        private String lastName;
        private LocalDate dob;
        private Person mother;
        private Person[] family;
    
        public Person(String firstName, String lastName, LocalDate dob) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dob = dob;
        }
    
        public static Person makeCopy(Person p) {
            Person copy = new Person(new String(p.firstName),
                    new String(p.lastName),
                    LocalDate.of(p.dob.getYear(), p.dob.getMonth(), p.dob.getDayOfMonth()));
            if (p.mother != null) {
                copy.mother = Person.makeCopy(p.mother);
            }
            if (p.family != null) {
                copy.family = new Person[p.family.length];
                for (int i = 0; i < p.family.length; i++) {
                    if (p.family[i] != null) {
                        copy.family[i] = Person.makeCopy(p.family[i]);
                    }
                }
            }
            return copy;
        }
    
        // omitting other methods for brevity
    
    }
    

    Comparación de las diferencias de implementación

    Crear copias del objeto Java a trav√©s de la ruta de implementaci√≥n Cloneable y anulando clone() con raz√≥n se ha ganado un poco de mala reputaci√≥n. Esto se debe a la naturaleza extra√Īa en la que la interfaz cambia la visibilidad del clone() m√©todo en s√≠ junto con la necesidad, a menudo subestimada, de clonar en profundidad los campos de clase con tipo de referencia mutable. Por estas razones, prefiero usar constructores de copias y m√©todos de f√°brica para crear copias de objetos. Solo cuando estoy trabajando con una clase que ha implementado espec√≠ficamente la Cloneable interfaz con la que proceder√© usando el clone() m√©todo.

    Conclusión

    En este art√≠culo he descrito el por qu√© y el c√≥mo de crear copias de objetos en Java. He cubierto los detalles de la forma tradicional pero un tanto idiom√°ticamente extra√Īa de copiar a trav√©s de la implementaci√≥n de la Cloneable interfaz en conjunto con el clone() m√©todo, as√≠ como c√≥mo utilizar constructores de copia y m√©todos de f√°brica est√°ticos.

    Como siempre, gracias por leer y no dude en comentar o criticar a continuación.

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *