Java Object clone()方法–用Java克隆
克隆是创建对象副本的过程。
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()方法实现。
如果存在可变字段,则只需要对这些字段进行深度复制即可。
这里有一个简单的程序来测试这种克隆方式是否正常工作。