简单的对象拷贝
在应用开发中,有时候需要得到一个对象的副本,然后对该副本做一些修改而不影响原始对象,可能大家会想到像下面示例中这样做。
假设定义一个Person对象,只有name和age两个字段。
public class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
按如下方法进行对象拷贝:
Person original = new Person("paul", 18);
Person copy = original;
copy.age = 20;
System.out.println("original-name:" + original.name + ",age:" + original.age);
System.out.println("copy-name:" + copy.name + ",age:" + copy.age);
输出结果:
original-name:paul,age:20
copy-name:paul,age:20
经测试发现,对拷贝对象做的修改,也导致原始对象发生了改变。其实拷贝变量与原始变量都指向了同一个引用,改变一个变量所引用的对象都将对另一个变量产生影响。我们可以打印original对象与copy对象的hashcode,就会发现它们的值相同,从而证明它们确实指向同一个引用,因此此法不通。
对象克隆
此时也许你想到了clone方法,但clone没法直接调用,它是Object的一个protected方法,可重新为Person类定义一个public的clone方法,具体如下:
public class Person implements Cloneable {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
注意还必须为Person类实现Cloneable接口,否则将报java.lang.CloneNotSupportedException
异常。需要知道的是Cloneable接口是Java提供的几个标记接口之一,所谓标记接口,就是该接口没有方法让实现类来实现,使用该接口的唯一目的便是可以利用instanceof进行类型检验,如:
if (obj instanceof Cloneable)
此时我们再来克隆一个对象,继续测试:
Person original = new Person("paul", 18);
Person copy = null;
try {
copy = original.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
copy.age = 20;
System.out.println("original-name:" + original.name + ",age:" + original.age);
System.out.println("copy-name:" + copy.name + ",age:" + copy.age);
输出结果:
original-name:paul,age:18
copy-name:paul,age:20
终于发现,当修改克隆得到的对象的年龄,不会对原始对象的年龄产生影响了,通过打印两个对象的hashcode,可知它们是两个不同的对象,也许此刻你觉得已经找到终极解决方案了!
但是,不要高兴得太早,没那么简单……
浅拷贝与深拷贝
假如当Person对象中又包含了子对象的引用,如下面的示例,为Person对象添加一个Pet字段:
public class Person implements Cloneable {
public String name;
public int age;
public Pet pet;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
其中Pet类定义如下:
public class Pet {
public String nickName;
public Pet(String nickName) {
this.nickName = nickName;
}
}
再来看测试代码:
Person original = new Person("paul", 18);
original.pet = new Pet("mimi");
Person copy = null;
try {
copy = original.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
copy.age = 20;
copy.pet.nickName = "beibei";
System.out.println("original-name:" + original.name + ",age:" + original.age + ",Pet nickName:" + original.pet.nickName);
System.out.println("copy-name:" + copy.name + ",age:" + copy.age + ",Pet nickName:" + copy.pet.nickName);
System.out.println("original-pet hashcode:" + original.pet.hashCode());
System.out.println("copy-pet hashcode:" + copy.pet.hashCode());
System.out.println("original hashcode:" + original.hashCode());
System.out.println("copy hashcode:" + copy.hashCode());
输出结果:
original-name:paul,age:18,Pet nickName:beibei
copy-name:paul,age:20,Pet nickName:beibei
original-pet hashcode:7643448
copy-pet hashcode:7643448
original hashcode:1761895363
copy hashcode:513775457
可见,通过克隆得到的Person对象的age字段的修改不会对原始对象产生影响,但当把克隆得到的Person对象的Pet的昵称从“mimi”改为了“beibei”,结果导致原始对象的Pet的昵称也变成了“beibei”,通过克隆对象与原始对象的Pet的hashcode值均为7643448可知,两个对象所引用的Pet其实是同一个对象。
到这里是否觉得有点乱了?clone()本身是Object的方法,而Object类其实对具体的类对象一无所知,所以就会将各个域进行对应的拷贝。如果对象中的所有数据域都属于数值或基本数据类型,这样的拷贝是没有问题的。但是如果对象中包含了对子对象的引用,拷贝的结果会将两个域引用到同一个子对象,导致原始对象与克隆对象共享这部分数据。
默认的克隆其实是浅拷贝(shallow copy),它不会克隆包含在对象中的内部对象,上述场景就是由于我们使用了浅拷贝。当然如果对象中的内部对象是不可变的,那么使用浅拷贝也无所谓,但是如果对象中的内部对象是可变的,如上述示例Person中的Pet,那么使用浅拷贝进行克隆就会出现问题。此时就需要我们重定义clone,实现所谓的深拷贝(deep copy)。
实现深拷贝的方案如下,首先为Pet类重定义clone方法:
public class Pet implements Cloneable {
public String nickName;
public Pet(String nickName) {
this.nickName = nickName;
}
public Pet clone() throws CloneNotSupportedException {
return (Pet) super.clone();
}
}
然后改写Person中的clone方法:
public class Person implements Cloneable {
public String name;
public int age;
public Pet pet;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.pet = pet.clone();
return person;
}
}
测试代码不变,依然如下:
Person original = new Person("paul", 18);
original.pet = new Pet("mimi");
Person copy = null;
try {
copy = original.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
copy.age = 20;
copy.pet.nickName = "beibei";
System.out.println("original-name:" + original.name + ",age:" + original.age + ",Pet nickName:" + original.pet.nickName);
System.out.println("copy-name:" + copy.name + ",age:" + copy.age + ",Pet nickName:" + copy.pet.nickName);
System.out.println("original-pet hashcode:" + original.pet.hashCode());
System.out.println("copy-pet hashcode:" + copy.pet.hashCode());
System.out.println("original hashcode:" + original.hashCode());
System.out.println("copy hashcode:" + copy.hashCode());
输出结果:
original-name:paul,age:18,Pet nickName:mimi
copy-name:paul,age:20,Pet nickName:beibei
original-pet hashcode:179704568
copy-pet hashcode:932666694
original hashcode:7643448
copy hashcode:1761895363
可以看到,此时修改克隆对象的Pet的nickName,仅会影响到克隆对象,不会再影响原始对象的值了。再次对比克隆对象与原始对象的Pet的哈希值,可知现在它们已经是两个不同的对象了,也证明了我们实现了对子对象的深拷贝。
总结
其实克隆的应用场景并不多,但我们也应谨慎使用克隆,一旦为某个类实现了clone方法,那么就意味着可以为它克隆对象。另外如果需要实现深拷贝,那么还需要重新实现clone方法。