Java Object clone()方法–用Java克隆

时间:2020-02-23 14:36:27  来源:igfitidea点击:

克隆是创建对象副本的过程。
Java Object类带有本机的clone()方法,该方法返回现有实例的副本。

由于Object是Java中的基类,因此默认情况下所有对象都支持克隆。

Java对象克隆

如果要使用Java Object clone()方法,则必须实现java.lang.Cloneable标记接口。
否则,它将在运行时抛出CloneNotSupportedException

同样,对象克隆也是一种受保护的方法,因此您将不得不覆盖它。

让我们来看一个示例程序中的Java对象克隆。

package com.theitroad.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();
	 }

}

我们正在使用Object clone()方法,所以我们实现了Cloneable接口。
我们正在调用超类clone()方法,即对象clone()方法。

使用对象clone()方法

让我们创建一个测试程序,以使用对象clone()方法创建实例的副本。

package com.theitroad.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("hyman");
		Map<String, String> props = new HashMap<>();
		props.put("salary", "10000");
		props.put("city", "Bangalore");
		emp.setProps(props);

		Employee clonedEmp = (Employee) emp.clone();

		//Check whether the emp and clonedEmp attributes are same or different
		System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
		
		System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
		
		//Let's see the effect of using default cloning
		
		//change emp props
		emp.getProps().put("title", "CEO");
		emp.getProps().put("city", "New York");
		System.out.println("clonedEmp props:" + clonedEmp.getProps());

		//change emp name
		emp.setName("new");
		System.out.println("clonedEmp name:" + clonedEmp.getName());

	}

}

输出:

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: true
clonedEmp props:{city=New York, salary=10000, title=CEO}
clonedEmp name:hyman

运行时发生CloneNotSupportedException

如果我们的Employee类不会实现Cloneable接口,则上述程序将抛出CloneNotSupportedException运行时异常。

Exception in thread "main" java.lang.CloneNotSupportedException: com.theitroad.cloning.Employee
	at java.lang.Object.clone(Native Method)
	at com.theitroad.cloning.Employee.clone(Employee.java:41)
	at com.theitroad.cloning.CloningTest.main(CloningTest.java:19)

了解对象克隆

让我们看一下上面的输出,并了解Objectclone()方法的情况。

  • emp和clonedEmp == test:false 。
    :这意味着emp和clonedEmp是两个不同的对象,而不是引用同一对象。
    这与Java对象克隆要求一致。

  • emp和clonedEmp HashMap == test:true:因此emp和clonedEmp对象变量都引用同一对象。
    如果我们更改基础对象的值,这可能是一个严重的数据完整性问题。
    值的任何更改也可能会反映到克隆的实例中。

  • clonedEmp props:{city = New York,salary = 10000,title = CEO}}:我们没有对clonedEmp属性进行任何更改,但是由于emp和clonedEmp变量都指向同一个对象,因此它们得到了更改。
    发生这种情况是因为默认的Object clone()方法创建了浅表副本。
    当您要通过克隆过程创建完全分离的对象时,可能会出现问题。
    这可能会导致不想要的结果,因此需要正确地重写Object clone()方法。

  • clonedEmp name:hyman:这是怎么回事?我们更改了emp名称,但clonedEmp名称未更改。
    这是因为String是不可变的。
    因此,当我们设置emp名称时,将创建一个新字符串并在'this.name = name;'中更改emp名称引用。
    因此clonedEmp名称保持不变。
    对于任何原始变量类型,您也会发现类似的行为。
    因此,只要对象中只有原始变量和不可变变量,我们就擅长使用Java对象默认克隆。

对象克隆类型

有两种类型的对象克隆–浅克隆和深克隆。
让我们了解它们的每一个,并找出在我们的Java程序中实现克隆的最佳方法。

1.浅克隆

Java Object clone()方法的默认实现是使用浅表复制。
它使用反射API创建实例的副本。
下面的代码片段展示了浅层克隆实现。

@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.深克隆

在深度克隆中,我们必须一一复制字段。
如果我们有一个带有嵌套对象(例如List,Map等)的字段,那么我们必须编写代码以将它们也一张一张地复制。
这就是为什么将其称为深度克隆或者深度复制。

我们可以像下面的代码那样覆盖Employee克隆方法,以进行深度克隆。

public Object clone() throws CloneNotSupportedException {

	Object obj = super.clone(); //utilize clone Object method

	Employee emp = (Employee) obj;

	//deep cloning for immutable fields
	emp.setProps(null);
	Map<String, String> hm = new HashMap<>();
	String key;
	Iterator<String> it = this.props.keySet().iterator();
	//Deep Copy of field by field
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, this.props.get(key));
	}
	emp.setProps(hm);
	
	return emp;
}

使用此clone()方法实现,我们的测试程序将产生以下输出。

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: false
clonedEmp props:{city=Bangalore, salary=10000}
clonedEmp name:hyman

在大多数情况下,这就是我们想要的。
clone()方法应返回一个与原始实例完全分离的新对象。

因此,如果您打算在程序中使用对象克隆和克隆,请明智地进行操作,并通过照顾可变字段来适当地覆盖它。

如果您的程序扩展了其他程序,又扩展了其他程序,这可能是一项艰巨的任务。
您将必须在对象继承层次结构中进行所有操作,以照顾所有可变字段的深层副本。

使用序列化进行克隆?

轻松执行深度克隆的一种方法是序列化。
但是序列化是一个昂贵的过程,您的类应实现" Serializable"接口。
所有字段和超类也必须实现Serializable。

使用Apache Commons Util

如果您已在项目中使用Apache Commons Util类,并且该类可序列化,则使用以下方法。

Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);

复制构造函数以进行克隆

我们可以定义一个复制构造函数来创建对象的副本。
为什么要完全依赖Object clone()方法?

例如,我们可以有一个类似于以下代码的Employee复制构造函数。

public Employee(Employee emp) {
	
	this.setId(emp.getId());
	this.setName(emp.getName());
	
	Map<String, String> hm = new HashMap<>();
	String key;
	Iterator<String> it = emp.getProps().keySet().iterator();
	//Deep Copy of field by field
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, emp.getProps().get(key));
	}
	this.setProps(hm);

}

每当我们需要员工对象的副本时,都可以使用" Employee clonedEmp = new Employee(emp);"来获取它。

但是,如果您的类具有很多变量,尤其是原始变量和不可变变量,那么编写拷贝构造函数可能是一项繁琐的工作。

Java对象克隆最佳实践

  • 仅当您的类具有基元和不可变变量或者需要浅表复制时,才使用默认的Object clone()方法。
    在继承的情况下,您将必须检查所有扩展到对象级别的类。

  • 如果您的类主要具有可变属性,则也可以定义复制构造函数。

  • 通过在覆盖的克隆方法中调用super.clone()来利用Object clone()方法,然后进行必要的更改以对可变字段进行深度复制。

  • 如果您的类可序列化,则可以使用序列化进行克隆。
    但是,它将带来性能上的损失,因此在使用序列化进行克隆之前,请进行一些基准测试。

  • 如果要扩展类,并且使用深层复制正确定义了克隆方法,则可以使用默认克隆方法。
    例如,我们在Employee类中正确定义了clone()方法,如下所示。

我们可以创建一个子类,并利用超类进行深度克隆,如下所示。

" EmployeeWrap"类没有任何可变的属性,它利用超类clone()方法实现。
如果存在可变字段,则只需要对这些字段进行深度复制即可。
这里有一个简单的程序来测试这种克隆方式是否正常工作。