Java中的clone()方法 – 在Java中进行克隆操作
克隆是创建对象的过程。Java中的Object类带有本地的clone()方法,该方法返回现有实例的副本。由于Object是Java中的基类,默认情况下所有对象都支持克隆。
Java对象克隆
package com.Olivia.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()方法,也就是Object类的clone()方法。
使用Object clone()方法进行复制
让我们创建一个测试程序,使用对象的clone()方法来创建实例的副本。
package com.Olivia.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<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:Pankaj
运行时的克隆不受支持异常
如果我们的员工类不实现Cloneable接口,上述程序将抛出一个在运行时抛出的CloneNotSupportedException异常。
Exception in thread "main" java.lang.CloneNotSupportedException: com.Olivia.cloning.Employee
at java.lang.Object.clone(Native Method)
at com.Olivia.cloning.Employee.clone(Employee.java:41)
at com.Olivia.cloning.CloningTest.main(CloningTest.java:19)
了解对象克隆
让我们来看一下上面的输出并了解一下Object clone()方法的运作情况。
-
- 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:Pankaj:这里发生了什么?我们更改了emp的姓名,但clonedEmp的姓名没有变化。这是因为String是不可变的。所以当我们设置emp的姓名时,会创建一个新的字符串,并且emp的姓名引用也会更改为this.name = name。因此,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. 深度克隆
在深度克隆中,我们必须逐个复制字段。如果有嵌套对象的字段,例如列表、地图等等,那么我们还必须逐个编写代码来复制它们。这就是为什么它被称为深度克隆或深拷贝。我们可以通过以下代码重写Employee的clone方法来进行深度克隆。
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:Pankaj
在大多数情况下,这就是我们想要的结果。clone()方法应该返回一个与原始实例完全独立的新对象。因此,如果你在程序中考虑使用Object clone和克隆,在重写和处理可变字段时要明智地操作。如果你的类扩展了其他类,而这些类又扩展了其他类,以此类推,那可能是一项艰巨的任务。你必须一直沿着Object继承层级前进,以处理所有可变字段的深度复制。
使用序列化进行克隆?
在中文中,一种轻松执行深度克隆的方法是通过序列化。但是序列化是一个昂贵的过程,你的类应该实现Serializable接口。所有的字段和超类也必须实现Serializable。
使用Apache Commons Util
如果您的项目已经在使用Apache Commons Util类,并且您的类是可序列化的,那么请使用以下方法。
Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);
复制构造函数用于克隆
我们可以定义一个拷贝构造函数来创建对象的副本。为什么还要依赖于对象的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()方法。在继承的情况下,你将需要检查所有你扩展的类,直到Object级别为止。
如果你的类主要具有可变属性,还可以定义复制构造函数。
通过在重写的克隆方法中调用super.clone()来利用Object clone()方法,然后对可变字段进行深度复制所需的更改。
如果你的类可序列化,可以使用序列化来进行克隆。然而,这可能会带来性能损失,所以在使用序列化进行克隆之前,请进行一些基准测试。
如果你扩展了一个类,并且它已经正确地使用深度复制定义了克隆方法,那么你可以利用默认的clone方法。例如,我们在Employee类中正确定义了clone()方法如下。
我们可以创建一个子类并利用父类的深度复制如下。
EmployeeWrap类没有可变属性,并且它正在利用父类的clone()方法实现。如果有可变字段,则只需要对这些字段进行深度复制。这是一个简单的程序来测试这种克隆方式是否正常工作。
所以它完全按照我们的期望工作。
这就是关于Java中的对象克隆。我希望你对Java对象的clone()方法以及如何正确地覆盖它没有任何不良影响有了一些了解。
你可以从我的 GitHub 代码库下载这个项目。
参考文档:Object clone 的 API 文档