【翻译】Java对象克隆指南
原文地址:A Guide to Object Cloning in Java
克隆(Clone)是一种严格的复制。在Java中,克隆的含义是创建一个对象,其状态与原始对象保持一致。方法clone()
提供了这样的功能。本文我们将深入分析Java克隆的几个重要的特性。
一、细述克隆
克隆是创建一个原始对象的副本,其字典解释为:“制作一个完全一样的复本”。
Java克隆的缺省含义是“域到域的复制”。由于Object类无法知道每一个被调用clone()
方法类的结构,因此,当调用克隆方法时,JVM将作一下动作:
1) 如果类所包含的成员变量均为原生数据类型,那么将创建一个全新的且完全一样的对象,并返回这个新对象的引用。
2) 如果类包含非原生数据类型的成员变量,那么,新创建的复制对象仅复制这些成员的引用。因此,原始对象和复制对象的成员变量都指向同一个数据对象。
除了上述缺省行为,我们通常使用时会重写clone()
方法,以实现我们需要的处理逻辑。下面我们来看看如何重写clone()
方法。
二、Java克隆的基础
每一种支持对象克隆的语言都对克隆作了相应的约束,Java也不例外。在Java中,如果一个类型需要支持克隆,那么该类需要完成一下几件事:
a) 该类必须 实现Cloneable
接口。
b) 该类必须重写Object类的clone()
方法。更多参见:Cloneable interface is broken in Java
在Java docs中,关于clone()
方法给了如下解释:
/*
Creates and returns a copy of this object. The precise meaning of 'copy' ; may depend on the class of the object.
The general intent is that, for any object x, the expression:
1) x.clone() != x will be true
2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
3) x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object [More ...] clone() throws CloneNotSupportedException;
1、第一行声明要求保证为克隆对象分配独立的内存地址。
2、第二行声明建议原始对象和克隆对象属于相同的类型,但非强制性要求 。
3、第三行声明建议在使用equals()
判断原始对象和克隆对象是否相等时应返回true,但也非强制性要求。
让我们来看看实际的代码。
我们的第一个类Department
有三个属性:id、name。
public class Department {
private int id;
private String name;
public Department(int id, String name){
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setId(int id) {
this.id = id;
}
}
我们的第一个类Employee
有三个属性:Id、name和department。
public class Employee implements Cloneable {
private int employeeId;
private String employeeName;
private Department department;
public Employee(int id,String name,Department dept){
this.employeeId = id;
this.employeeName = name;
this.department = dept;
}
@Override
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}
public Department getDepartment() {
return department;
}
public int getEmployeeId() {
return employeeId;
}
public String getEmployeeName() {
return employeeName;
}
}
因此,如果我们需要克隆Employee类,那么,我们需要编写如下代码:
public class TestCloning {
public static void main(String[] args) throws CloneNotSupportedException {
// TODO Auto-generated method stub
Department dept = new Department(1, "Human Resource");
Employee original = new Employee(1,"Admin",dept);
//Lets create a clone of original object
Employee cloned = (Employee)original.clone();
//Let verify using employee id, if cloning actually worked
System.out.println(cloned.getEmployeeId());
//Verify JDK's rules
//Must be true and objects must have different memory addresses
System.out.println(original != cloned);
//As we are return same class; so it should be true
System.out.println(original.getClass() == cloned.getClass());
//Default equals method checks for references so it should be false.
//If we want to make it,
//we need to override equals method in Employee class.
System.out.println(original.equals(cloned));
}
}
输出结果:
1
true
true
false
OK!我们已经成功克隆了Employee类。但,这里我们有两个引用指向同一个对象。如果我们改变其中一个对象的department值,两个employee对象将同时改变。我们看看是如何改变的。
public class TestCloning {
public static void main(String[] args) throws CloneNotSupportedException {
Department hr = new Department(1, "Human Resource");
Employee org = new Employee(1,"Admin",hr);
Employee cloned = (Employee)org.clone();
//Let change the department name in cloned object
//and we will verify in original object
cloned.getDepartment().setName("Finance");
System.out.println(org.getDepartment().getName());
}
}
输出结果:Finance
。可以看出,克隆对象的变化同时也影响了原始对象。这样的克隆很容易在系统中造成混乱。那我们可以阻止混乱发生吗?
当然可以,我们可以使用深度克隆和构造函数复制来阻止这一切,在本文的最后将深入讨论。
三、浅度克隆
浅度克隆是Java提供的默认实现。在重写的clone
方法中,如果我们不克隆所有的对象类型(非原生类型)属性,那么我们实现的就是浅度克隆。
上面的示例也是浅度复制,因为我们在clone()
方法中没有克隆Employee类型的Department对象。现在,我们来看看所谓的深度克隆。
四、深度克隆
大多数场景下,我们真正想要的是深度克隆。我们需要通过克隆创建一个与原始对象完全独立的对象,两个对象的变化不应该相互影响。
让我们来在上述示例实现深度复制。首先,在Department
类中重写clone()
方法。
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
接着,我们修改Employee
类的clone()
方法。
@Override
protected Object clone() throws CloneNotSupportedException{
Employee cloned = (Employee)super.clone();
cloned.setDepartment((Department)cloned.getDepartment().clone());
return cloned;
}
现在,我们来测试深度克隆能否达到两个对象相互独立的效果。
public class TestCloning {
public static void main(String[] args) throws CloneNotSupportedException {
Department hr = new Department(1, "Human Resource");
Employee org = new Employee(1,"Admin",hr);
Employee cloned = (Employee)org.clone();
//Let change the department name in cloned object
//and we will verify in original object
cloned.getDepartment().setName("Finance");
System.out.println(org.getDepartment().getName());
}
}
输出结果:Human Resource
。此时,改变克隆对象的状态将不在影响原始对象了。
因此,实现深度克隆需要遵循以下规则:
+ 不需要逐个复制原生类型成员变量。
+ 原始类型中所有的成员变量类型都需要支持克隆,且在原始类型上下文的clone()
方法中应该调用所有成员变量类型super.clone()
方法。
+ 如果成员变量类型不支持克隆,那么,在clone()
方法中我们必须重新创建一个成员变量类型的实例并逐一复制原变量的属性。最后将该对象set到克隆对象中。更多参见:Deep cloning using in-memory serialization
五、使用复制构造函数
复制构造函数(Copy Constructor)是一种特殊的构造函数,该构造函数的参数是自身类型的实例。因此,我们可以传一个该类型的实例给复制构造函数,构造函数将返回一个与参数实例属性值的新实例。让我们看一个示例:
public class PointOne{
private Integer x;
private Integer y;
public PointOne(PointOne point){
this.x = point.x;
this.y = point.y;
}
public PointOne(Integer x, Integer y) {
this.x = x;
this.y = y;
}
}
这种方法看起来很简单,但如果有子类继承它情况就变得复杂了一些。当我们定义一个类扩展上述类时,我们同样需要定义一个类似的构造函数。在子类中,我们需要复制子类特有的属性,且需要调用父类的复杂构造函数。代码如下:
public class PointTwo extends PointOne {
private Integer z;
public PointTwo(PointTwo point) {
super(point);
this.z = point.z;
}
public PointTwo(Integer x, Integer y, Integer z) {
super(x,y);
this.z = z;
}
}
那么,这样就没有问题了吗?不是。问题是使用继承,代码的行为在运行时确定。因此,上述代码中,如果我们传递PointTwo
实例至PointOne
的复制构造函数时,我们得到的将是PointOne
的实例。来看看如下代码:
public class CloneTest {
public static void main(String[] args) {
PointOne one = new PointOne(1,2);
PointTwo two = new PointTwo(1,2,3);
PointOne clone1 = new PointOne(one);
PointOne clone2 = new PointOne(two); //return an instance of PointOne
PointTwo clone3 = new PointTwo(two); //return an instance of PointTwo
System.out.println(clone1.getClass());
System.out.println(clone2.getClass());
System.out.println(clone3.getClass());
}
}
输出结果:
class com.woodman.clone.PointOne
class com.woodman.clone.PointOne
class com.woodman.clone.PointTwo
和复制构造函数类似的另一种方法是创建一个静态工厂方法。方法中可以通过参数的类型来使用另一个类的构造函数来创建一个实例并返回。工厂方法中将原始实例的所有状态复制到新的实例中。
public class PointOne implements Cloneable{
private Integer x;
private Integer y;
public PointOne(PointOne point) {
this.x = point.x;
this.y = point.y;
}
public PointOne(Integer x, Integer y) {
this.x = x;
this.y = y;
}
public PointOne copyPoint(PointOne point) throws CloneNotSupportedException{
if(!(point instanceof Cloneable)){
throw new CloneNotSupportedException("Invalid cloning");
}
//can do multiple other things here.
return new PointOne(point.x,point.y);
}
}
六、利用序列化实现克隆
序列化是另一个简单有效的克隆方法。使用这种方法,我们先序列化一个对象,然后再反序列化。当然,需要被克隆对象的类型需要实现Serializable
接口。
在进一步介绍之前,需要提醒大家的是这种克隆是一种重量级的方法。首先,序列化是代价高昂的,它带来的资源消耗往往是使用clone()
方法的数百倍。其次,并不是所有的对象都可以被序列化。最后,实现类的序列化是需要技巧的,并不是所有的类都能被正确的反序列化。
更多参见:A guide to implement serialization in java
让我们来看看代码示例:
@SuppressWarnings("unchecked")
public static T clone(T t) throws Exception {
//Check if T is instance of Serializeble other throw CloneNotSupportedException
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//Serialize it
serializeToOutputStream(t, bos);
byte[] bytes = bos.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
//Deserialize it and return the new instance
return (T)ois.readObject();
}
七、使用Apache commons实现克隆
Apache commons提供了用于克隆的工具函数。下面是使用Apache commons克隆工具的示例:
PointOne cloned = (PointOne) SerializationUtils.clone(One); //PointOne need implements Serializable interface.
八、最佳实践
1、当我们不知道是否可以调用特定类的clone()
方法,因为我们不确定该类是否继承Cloneable
接口。我们可以对其类型进行如下检查,确认其是否是Cloneable
接口的实例。
PointOne clone4;
if(one instanceof Cloneable){
clone4 = (PointOne) one.clone();
}
//Don't do this.Cloneable don't have any methods.
clone4 = (Cloneable)one.clone();
2、在克隆时不要使用对象的构造函数。这就需要我们来确保所有的成员变量都被正确的设置。同样,如果我们想通过记录构造函数被调用的次数来统计对象的个数时,我们需要在克隆时增加这个记录器。