Métodos de objetos de Java: clone ()

M

 

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.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad