浅谈Java对象克隆

564 阅读4分钟

前言

  在Java程序开发中,有时候需要我们得到另一个对象的克隆,在进行克隆时,我们往往希望得到的克隆对象里的数据和原对象一模一样,但是对其中任何一个对象的更改都不会影响另一个对象,也就是克隆之后,两个对象完全无关。对象的克隆并不是简单的一条赋值语句就可以,接下来简单探讨一下Java的对象克隆(拷贝)。

1.没拷贝

  先看一下最简单的写法:

class Person{
    private String name;
    private int age;
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public void setAge(int age){
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString(){
        return "name:"+name + " age:" + age;
    }
}
public class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person per = new Person("testClone",18);
        Person perClone = per;
        perClone.setAge(19);
        System.out.println("原对象:"+per);
        System.out.println("克隆对象:"+perClone);
    }
}

  如代码,定义了一个Person类,具有name和age属性,新建了一个Person对象per,用语句Person perClone = per;来企图进行拷贝,再把克隆对象的age改为19,结果自然是不尽如人意

image.png
由于Java的对象变量保存的是引用,Person perClone = per;顶多是给原对象加了个别名,完全没起到拷贝的作用。

2.浅拷贝

  Object类里提供了一个clone方法,可以用来进行对象的克隆。

image.png
可以看出,Object自带的方法是一个native方法,native方法是底层方法,这里不多介绍了。这个clone方法的机理是返回一个新的对象,对对象里的数据进行逐个的赋值。我们在Person类中加入如下代码:

@Override
public Person clone() throws CloneNotSupportedException{
    return (Person)super.clone();
}

  由于Object里的clone方法是protected类型,无法用对象直接调用,必须先在类中重写,将其改为public权限,就可以随意调用。注意的是方法需要抛出一个CloneNotSupportedException,否则会报如下的错:

image.png
同时,需要实现拷贝功能的类也要实现Cloneable接口,否则也会报同样的错,此接口定义如下:

public interface Cloneable {
}

  可以看出,接口里没有任何东西,这是个标记接口,但是不实现就是无法使用拷贝功能。我们把主方法改成如下:

public static void main(String[] args) throws CloneNotSupportedException{
    Person per = new Person("testClone",18);
    Person perClone = per.clone();
    perClone.setAge(19);
    perClone.setName("clone");
    System.out.println("原对象:"+per);
    System.out.println("克隆对象:"+perClone);
}

image.png
咦?这么一看,这不是达到了我们想要的效果了吗?对克隆对象的修改没有影响到原对象啊。这种方式是不是就够了?
首先,age是基本数据类型,采用赋值进行克隆的操作是没问题的;其次,String类是不可变类,对字符串的更改最终结果是使得String类对象变量指向另一个字符串,String类对象存储的是一个字符串的引用,对对象的浅拷贝操作,使得perClone对象中的name也指向"testClone"字符串,之后的perClone.setName("clone"),令perClone对象中的name指向"clone"字符串,所以这种方式看似达成了目标。 我们可以向Person类中再加一个属性pet,pet是Pet类的对象,这是个可变类。

class Pet{
    String type;
    public Pet(String type) {
        this.type = type;
    }
    public void setType(String type) {
        this.type = type;
    }
    @Override
    public String toString(){
        return this.type;
    }
}

  定义一个Pet类,给Person类加上一个属性private Pet pet以及对应的set和get方法。主函数修改如下:

Person per = new Person("testClone",18,new Pet("cat"));
Person perClone = per.clone();
perClone.setAge(19);
perClone.setName("clone");
perClone.getPet().setType("dog");
System.out.println("原对象:"+per);
System.out.println("克隆对象:"+perClone);

image.png
我们可以发现,对克隆对象的修改影响了原对象,说明这种浅拷贝是不能达到效果的,还是不能让原对象和克隆对象完全无关。。。

image.png
简单画出两个对象的属性关系,就不难理解为啥一个对象对pet的改变会影响到另一个对象了。可见,使用Object类里的clone有些时候并不能满足我们的要求。。。

3.深拷贝

  由于pet是一个可变对象,所以它也需要被拷贝,而不是简单的赋值,对Pet类代码做出修改:

class Pet implements Cloneable{
    String type;
    public Pet(String type) {
        this.type = type;
    }
    public void setType(String type) {
        this.type = type;
    }
    @Override
    public String toString(){
        return this.type;
    }
    @Override
    public Pet clone() throws CloneNotSupportedException{
        return (Pet)super.clone();
    }
}

  现在,pet类也具有了克隆的能力,再将Person类中的clone方法修改如下:

@Override
public Person clone() throws CloneNotSupportedException{
    Person cloned = (Person)super.clone();
    cloned.pet = pet.clone();
    return cloned;
}

  再次运行程序,结果如下:

image.png
现在才是真正的完成了克隆,达到了原对象和克隆对象的修改不会相互影响的效果。此时的属性关系图为:

image.png

总结

  1.对对象进行克隆时,先检查Object类里的clone方法是否可用,如果对象里的属性都是基本数据类型,或者String类这种不可变类的对象,那么可以直接调用Object类的clone方法

  2.如果对象里有可变类的属性,就需要重写Object类的clone方法

  3.想调用clone方法,必须实现Cloneable接口,并抛出CloneNotSupportedException