Método clone() de Java Object – Clonación en Java

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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 en this.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

  1. 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.

  2. También puedes definir un constructor de copia si tu clase tiene principalmente propiedades mutables.

  3. 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.

  4. 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.

  5. 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