Clonar es el proceso de crear una copia de un objeto. La clase de objeto de Java viene con el método nativo clone()
que devuelve la copia de la instancia existente. Dado que Object es la clase base en Java, todos los objetos admiten clonación de forma predeterminada.
Clonación de Objetos en Java
Si deseas utilizar el método clone() de Object en Java, debes implementar la interfaz de marcador
java.lang.Cloneable
. De lo contrario, lanzará CloneNotSupportedException
en tiempo de ejecución. Además, el método clone de Object es un método protegido, por lo que tendrás que anularlo. Veamos la clonación de objetos en Java con un programa de ejemplo.
package com.journaldev.cloning;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class Employee implements Cloneable {
private int id;
private String name;
private Map<String, String> props;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, String> getProps() {
return props;
}
public void setProps(Map<String, String> p) {
this.props = p;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Estamos utilizando el método clone() de Object, por lo que hemos implementado la interfaz Cloneable
interface. Estamos llamando al método clone() de la superclase, es decir, al método Object clone().
Usando el Método clone() de Object
Creemos un programa de prueba para usar el método `clone()` del objeto para crear una copia de la instancia.
package com.journaldev.cloning;
import java.util.HashMap;
import java.util.Map;
public class CloningTest {
public static void main(String[] args) throws CloneNotSupportedException {
Employee emp = new Employee();
emp.setId(1);
emp.setName("Pankaj");
Map props = new HashMap<>();
props.put("salary", "10000");
props.put("city", "Bangalore");
emp.setProps(props);
Employee clonedEmp = (Employee) emp.clone();
// Compruebe si los atributos emp y clonedEmp son iguales o diferentes
System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
// Veamos el efecto de usar la clonación predeterminada
// cambiar las propiedades de emp
emp.getProps().put("title", "CEO");
emp.getProps().put("city", "New York");
System.out.println("clonedEmp props:" + clonedEmp.getProps());
// cambiar el nombre de emp
emp.setName("new");
System.out.println("clonedEmp name:" + clonedEmp.getName());
}
}
Salida:
emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: true
clonedEmp props:{city=New York, salary=10000, title=CEO}
clonedEmp name:Pankaj
CloneNotSupportedException en tiempo de ejecución
Si nuestra clase Employee no implementa la interfaz `Cloneable`, el programa anterior lanzará una excepción `CloneNotSupportedException` en tiempo de ejecución.
Exception in thread "main" java.lang.CloneNotSupportedException: com.journaldev.cloning.Employee
at java.lang.Object.clone(Native Method)
at com.journaldev.cloning.Employee.clone(Employee.java:41)
at com.journaldev.cloning.CloningTest.main(CloningTest.java:19)
Comprender la clonación de objetos
Veamos la salida anterior y comprendamos qué está sucediendo con el método `clone()` del objeto.
emp y clonedEmp == test: false
: Significa que emp y clonedEmp son dos objetos diferentes, no se refieren al mismo objeto. Esto está de acuerdo con el requisito de clonación de objetos en Java.emp y clonedEmp HashMap == prueba: true
: Ambas variables de objeto emp y clonedEmp se refieren al mismo objeto. Esto puede ser un problema grave de integridad de datos si cambiamos el valor subyacente del objeto. Cualquier cambio en el valor podría reflejarse también en la instancia clonada.clonedEmp props:{ciudad=Nueva York, salario=10000, título=CEO}
: No hicimos ningún cambio en las propiedades de clonedEmp, pero aún así, cambiaron porque tanto emp como clonedEmp se refieren al mismo objeto. Esto sucede porque el método clone() por defecto de Object crea una copia superficial. Puede ser un problema cuando deseas crear objetos totalmente independientes a través del proceso de clonación. Esto puede llevar a resultados no deseados, de ahí la necesidad de anular correctamente el método clone() de Object.clonedEmp nombre:Pankaj
: ¿Qué sucedió aquí? Cambiamos el nombre de emp pero el nombre de clonedEmp no cambió. Esto se debe a que String es inmutable. Entonces, cuando establecemos el nombre de emp, se crea una nueva cadena y la referencia del nombre de emp se cambia enthis.name = name;
. Por lo tanto, el nombre de clonedEmp permanece sin cambios. Encontrarás un comportamiento similar para cualquier tipo de variable primitiva también. Así que estamos bien con la clonación por defecto de objetos Java siempre y cuando solo tengamos variables primitivas e inmutables en el objeto.
Tipos de Clonación de Objetos
Hay dos tipos de clonación de objetos: clonación superficial y clonación profunda. Entendamos cada uno de ellos y descubramos la mejor manera de implementar la clonación en nuestros programas Java.
1. Clonación Superficial
La implementación predeterminada del método clone() de Java utiliza una copia superficial. Utiliza la API de reflexión para crear la copia de la instancia. El siguiente fragmento de código muestra la implementación de la clonación superficial.
@Override
public Object clone() throws CloneNotSupportedException {
Employee e = new Employee();
e.setId(this.id);
e.setName(this.name);
e.setProps(this.props);
return e;
}
2. Clonación Profunda
En la clonación profunda, debemos copiar los campos uno por uno. Si tenemos un campo con objetos anidados como List, Map, etc., entonces debemos escribir el código para copiarlos también uno por uno. Por eso se llama clonación profunda. Podemos anular el método clone de Employee como se muestra en el siguiente código para la clonación profunda.
public Object clone() throws CloneNotSupportedException {
Object obj = super.clone(); //utilize clone Object method
Employee emp = (Employee) obj;
// clonación profunda para campos inmutables
emp.setProps(null);
Map hm = new HashMap<>();
String key;
Iterator it = this.props.keySet().iterator();
// Copia profunda de campo por campo
while (it.hasNext()) {
key = it.next();
hm.put(key, this.props.get(key));
}
emp.setProps(hm);
return emp;
}
Con esta implementación del método clone(), nuestro programa de prueba producirá la siguiente salida.
emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: false
clonedEmp props:{city=Bangalore, salary=10000}
clonedEmp name:Pankaj
En la mayoría de los casos, esto es lo que queremos. El método clone() debería devolver un objeto nuevo totalmente desvinculado de la instancia original. Así que si estás pensando en usar Object clone y la clonación en tu programa, hazlo sabiamente y sobrescríbelo correctamente teniendo en cuenta los campos mutables. Puede ser una tarea desafiante si tu clase extiende otra clase que a su vez extiende otra clase y así sucesivamente. Tendrás que recorrer toda la jerarquía de herencia de Object para ocuparte de la copia profunda de todos los campos mutables.
¿Clonar usando Serialización?
Una manera de realizar fácilmente la clonación profunda es a través de la serialización. Pero la serialización es un procedimiento costoso y tu clase debe implementar la interfaz Serializable
. Todos los campos y superclases también deben implementar Serializable.
Usando Apache Commons Util
Si ya estás utilizando las clases de Apache Commons Util en tu proyecto y tu clase es serializable, entonces utiliza el siguiente método.
Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);
Constructor de Copia para Clonación
Podemos definir un constructor de copia para crear una copia del objeto. ¿Por qué depender del método clone() del objeto en absoluto? Por ejemplo, podemos tener un constructor de copia para la clase Employee como el siguiente código.
public Employee(Employee emp) {
this.setId(emp.getId());
this.setName(emp.getName());
Map hm = new HashMap<>();
String key;
Iterator it = emp.getProps().keySet().iterator();
// Copia profunda campo por campo
while (it.hasNext()) {
key = it.next();
hm.put(key, emp.getProps().get(key));
}
this.setProps(hm);
}
Cuando necesitemos una copia del objeto Employee, podemos obtenerla usando Employee clonedEmp = new Employee(emp);
. Sin embargo, escribir un constructor de copia puede ser un trabajo tedioso si tu clase tiene muchas variables, especialmente primitivas e inmutables.
Mejores Prácticas para la Clonación de Objetos en Java
-
Utiliza el método clone() predeterminado de Object solo cuando tu clase tiene variables primitivas e inmutables o deseas una copia superficial. En caso de herencia, deberás revisar todas las clases que estás extendiendo hasta el nivel de Object.
-
También puedes definir un constructor de copia si tu clase tiene principalmente propiedades mutables.
-
Utiliza el método clone() del objeto llamando a
super.clone()
en el método clone sobrescrito, luego realiza los cambios necesarios para la copia profunda de campos mutables. -
Si tu clase es serializable, puedes utilizar la serialización para la clonación. Sin embargo, esto afectará al rendimiento, así que realiza algunas pruebas de rendimiento antes de usar la serialización para la clonación.
-
Si estás extendiendo una clase y ha definido correctamente el método clon con copia profunda, entonces puedes utilizar el método clon predeterminado. Por ejemplo, hemos definido correctamente el método clone() en la clase Employee de la siguiente manera.
@Override public Object clone() throws CloneNotSupportedException { Object obj = super.clone(); Employee emp = (Employee) obj; // clonación profunda para campos inmutables emp.setProps(null); Map<String, String> hm = new HashMap<>(); String key; Iterator<String> it = this.props.keySet().iterator(); // Copia profunda de campo por campo while (it.hasNext()) { key = it.next(); hm.put(key, this.props.get(key)); } emp.setProps(hm); return emp; }
Podemos crear una clase hija y utilizar la clonación profunda de la superclase de la siguiente manera.
package com.journaldev.cloning; public class EmployeeWrap extends Employee implements Cloneable { private String title; public String getTitle() { return title; } public void setTitle(String t) { this.title = t; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } }
La clase
EmployeeWrap
no tiene propiedades mutables y está utilizando la implementación del método clone() de la superclase. Si hay campos mutables, entonces tendrás que ocuparte de la copia profunda solo de esos campos. Aquí tienes un programa simple para probar si esta forma de clonación funciona bien o no.package com.journaldev.cloning; import java.util.HashMap; import java.util.Map; public class CloningTest { public static void main(String[] args) throws CloneNotSupportedException { EmployeeWrap empWrap = new EmployeeWrap(); empWrap.setId(1); empWrap.setName("Pankaj"); empWrap.setTitle("CEO"); Map<String, String> props = new HashMap<>(); props.put("salary", "10000"); props.put("city", "Bangalore"); empWrap.setProps(props); EmployeeWrap clonedEmpWrap = (EmployeeWrap) empWrap.clone(); empWrap.getProps().put("1", "1"); System.out.println("Valor mutable de la propiedad empWrap = "+empWrap.getProps()); System.out.println("Valor mutable de la propiedad clonedEmpWrap = "+clonedEmpWrap.getProps()); } }
Salida:
Valor mutable de la propiedad empWrap = {1=1, city=Bangalore, salary=10000} Valor mutable de la propiedad clonedEmpWrap = {city=Bangalore, salary=10000}
Así que funcionó perfectamente como esperábamos.
Eso es todo sobre la clonación de objetos en Java. Espero que hayas obtenido alguna idea sobre el método clone() de los objetos en Java y cómo sobrescribirlo correctamente sin ningún efecto adverso.
Puedes descargar el proyecto desde mi Repositorio de GitHub.
Referencia: Documentación de la API para Object clone
Source:
https://www.digitalocean.com/community/tutorials/java-clone-object-cloning-java