Métodos de objetos de Java: clon ()

Este artículo es la continuación de una serie de artículos que describen los métodos a menudo olvidados de la clase de objeto base del lenguaje Java. Los siguientes son...

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 de objeto base del lenguaje Java. Los siguientes son los métodos del Objeto base de Java que están presentes en todos los objetos de Java debido a la herencia implícita de Objeto.

El enfoque de este artículo es el método clone() que se usa para generar copias claramente separadas (nuevas instancias) de un objeto. También debo señalar que el método clone() 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 {#por qué existe la necesidad de clonar un objeto}

Primero me gustaría comenzar con por qué puede ser necesario crear un clon o una 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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);
    }
}

Comienzo mi discusión creando un par de variables enteras ‘x’ e ‘y’ junto con una instancia de Persona y la asigno a una variable llamada ‘yo’. Luego asigno me a otra variable llamada me2 que luego cambio el campo firstName en me2 y muestro el contenido de ambas variables, así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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);
    }
}

Producción:

1
2
3
4
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 ¡Uy!… pero, para que todos estén en el mismo nivel de comprensión, permítanme explicarles lo que acaba de suceder allí. En Java, tiene dos categorías principales de tipos de datos: tipos de valor (también conocidos como primitivos) y tipos de referencia (también conocidos como objetos). En mi ejemplo anterior, los objetos Person como me y me2 son del tipo de referencia del objeto Person. A diferencia de los tipos de referencia de persona, x e y son tipos de valores de primitivas int.

Como se acaba de hacer evidente, la asignación con tipos de referencia se trata de manera diferente a un número entero, o tal vez se indica con mayor precisión 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, lo cual es muy diferente a la copia real de los contenidos que ocurre cuando hace lo mismo con los tipos de valor.

Es por eso que cuando cambié el valor del campo firstName de la variable de referencia me2 también vi el mismo cambio en la variable de referencia me, estaban haciendo referencia al mismo objeto en la memoria. Por estas razones, se vuelve importante poder crear copias reales (clones) de objetos de referencia y de ahí la necesidad del método clone().

Cómo clonar() un objeto

Como mencioné anteriormente, el método clone() de la clase Object es un poco controvertido en la comunidad de programación de Java. La razón de esto es que para implementar el método clone() necesitas implementar una interfaz peculiar llamada Cloneable del paquete "java.lang" que proporciona a tu clase la capacidad de exponer un clon público () método. Esto es necesario porque el método clone() en la clase Object está protegido y, por lo tanto, no se puede acceder a él desde el código del cliente que trabaja con su clase. Además, el comportamiento de la creación de objetos es bastante inusual en el sentido de que la instancia se crea sin invocar el codiciado operador “nuevo”, lo que deja a muchos, incluido yo mismo, un poco incómodos.

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

Bien, sin más bromas procederé a explicar cómo clonar objetos a través de clone() dentro de mi clase Person. Primero implementaré la interfaz Cloneable y agregaré el método clone() anulado públicamente que devuelve una instancia de tipo Object.

Para una clase simple como Persona que no contiene ningún campo mutable, todo lo que se requiere es crear un clon y devolver una llamada al método de clonación de la clase base Object, así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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í:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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);
    }
}

Producción:

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

Un voilà se hace un clon yo. Ahora, cuando actualizo la propiedad firstName de me2 usando el ejemplo anterior, el campo no cambia en el objeto me. Asegúrese de tener en cuenta la conversión explícita del clon devuelto de tipo Objeto a tipo Persona, lo cual es necesario porque la interfaz necesita devolver una referencia de tipo Objeto.

Desafortunadamente, esta implementación del método clone() solo funcionará en objetos que contengan valores tipificados simples que no tengan propiedades de referencia mutables. Si tuviera que agregar un par de campos mutables como “madre” de tipo “Persona” y una matriz “familiar” de objetos “Persona”, necesitaría hacer algunos cambios para permitir que se lleve a cabo una clonación segura.

Para demostrar esto, necesito actualizar mi clase Person así.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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, madre y familia, debo hacer copias explícitas 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 individuales de estos campos mutables, entonces los dos objetos Person resultantes estarían haciendo referencia a las mismas instancias de objetos mutables madre y familia, lo que sería un lío terrible para 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 {#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 copia y métodos de fábrica que crean copias de objetos. Sin embargo, en esta sección solo voy a cubrir los dos últimos porque personalmente no me importa mucho usar la serialización para crear copias de objetos.

Para empezar, cubriré el método del constructor de copias. Esta ruta de creación de copias de objetos utilizando 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 persona pública (Persona 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 los tipos de valor o se utiliza para crear nuevas instancias de sus campos en el caso de los tipos de referencia.

Aquí hay un ejemplo del uso de un constructor de copias para la clase Person:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 copias, excepto que la nueva copia se crea dentro de un método de fábrica estático que devuelve una nueva instancia como una copia, así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 {#comparación de las diferencias de implementación}

La creación de copias del objeto Java a través de la ruta de implementación de Cloneable y la anulación de clone() se ha ganado, con razón, un poco de mala reputación. Esto se debe a la extraña naturaleza en la que la interfaz cambia la visibilidad del propio método clone() junto con la necesidad a menudo subestimada de "profundamente" clonar campos de clase con tipo de referencia mutable. Por estas razones, prefiero usar constructores de copia y métodos de fábrica para crear copias de objetos. Solo cuando estoy trabajando con una clase que ha implementado específicamente la interfaz Cloneable, procederé con el uso del método clone().

Conclusión

En este artículo he descrito los porqués y cómos de crear copias de objetos en Java. He cubierto los detalles de la forma tradicional pero algo idiomáticamente extraña de copiar a través de la implementación de la interfaz Cloneable junto con el método clone(), así como también cómo usar constructores de copia y métodos estáticos de fábrica.

Como siempre, gracias por leer y no se avergüence de comentar o criticar a continuación.

Licensed under CC BY-NC-SA 4.0