深入理解Java对象克隆

来源:互联网 发布:iphone5c支持4g网络吗 编辑:程序博客网 时间:2024/05/17 05:19

简单的对象拷贝

在应用开发中,有时候需要得到一个对象的副本,然后对该副本做一些修改而不影响原始对象,可能大家会想到像下面示例中这样做。

假设定义一个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方法。

0 0