首页 > 技术文章 > Java的浅拷贝与深拷贝

linzeliang1222 2020-08-22 22:16 原文

Java的浅拷贝与深拷贝

Java中,所有的类都继承Object,Object中有clone方法,它被声明为了 protected ,所以我们但是如果要使用该方法就得重写且声明为public必须在要被Clone的类实现(implements)Cloneable接口,否则会报java.lang.CloneNotSupportedException异常,Cloneable接口是在java.lang中自动被导入的,而无论是浅拷贝还是深拷贝,都需要实现 clone() 方法。

如果不重写clone()方法,则在调用clone()方法实现的是浅复制(所有的引用对象保持不变,意思是如果原型里这些对象发生改变会直接影响到复制对象)。重写clone()方法,一般会先调用super.clone()进行浅复制,然后再复制那些易变对象,从而达到深复制的效果。

浅拷贝

一般的话,clone是自动进行浅拷贝,我们来看一下代码:

public class Resume implements Cloneable {

    private String name;
    private String sex;
    private String age;
    private String timeArea;
    private String company;

    public Resume(String name) {
        this.name = name;
    }

    //设置个人信息
    public void setPersonInfo(String age, String sex) {
        this.age = age;
        this.sex = sex;
    }

    //设置工作经历
    public void setWorkExperience(String timeArea, String company) {
        this.timeArea = timeArea;
        this.company = company;
    }

    //显示
    public void display() {
        System.out.println(name + " " + sex + " " + age);
        System.out.println("工作经历:" + timeArea + " " + company);
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    
    public static void main(String args[]) throws CloneNotSupportedException {
        Resume r = new Resume("大鸟");
        r.setPersonInfo("28", "男");
        r.setWorkExperience("1998-2000", "XX公司");
        Resume r2 = (Resume)r.clone();
        r2.setWorkExperience("2000-2003", "YY企业");
        Resume r3 = (Resume)r.clone();
        r3.setWorkExperience("2003-2020", "ZZ公司");
        r.display();
        r2.display();
        r3.display();
    }
}

结果是:

大鸟 男 28
工作经历:1998-2000 XX公司
大鸟 男 28
工作经历:2000-2003 YY企业
大鸟 男 28
工作经历:2003-2020 ZZ公司

将WorkExperience封装为一个类,看看有什么不同:

public class Resume implements Cloneable {

    private String name;
    private String sex;
    private String age;
    private WorkExperience we = null;

    public Resume(String name) {
        this.name = name;
        we = new WorkExperience();
    }

    //设置个人信息
    public void setPersonInfo(String age, String sex) {
        this.age = age;
        this.sex = sex;
    }

    public void setWorkExperience(String timeArea, String company) {
        we.setTimeArea(timeArea);
        we.setCompany(company);
    }


    //显示
    public void display() {
        System.out.println(name + " " + sex + " " + age);
        System.out.println("工作经历:" + we.getTimeArea() + " " + we.getCompany());
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    
    
    public static void main(String args[]) throws CloneNotSupportedException {
        Resume r = new Resume("大鸟");
        r.setPersonInfo("28", "男");
        r.setWorkExperience("1998-2000", "XX公司");
        Resume r2 = (Resume)r.clone();
        r2.setPersonInfo("31", "男");
        r2.setWorkExperience("2000-2003", "YY企业");
        Resume r3 = (Resume)r.clone();
        r3.setWorkExperience("2003-2020", "ZZ公司");
        r.display();
        r2.display();
        r3.display();
    }
}

class WorkExperience {
    private String timeArea;
    private String company;

    public String getTimeArea() {
        return timeArea;
    }

    public void setTimeArea(String timeArea) {
        this.timeArea = timeArea;
    }

    public String getCompany() {
        return company;
    }

    public void setCompany(String company) {
        this.company = company;
    }
}

这时结果的输出是:

大鸟 男 28
工作经历:2003-2020 ZZ公司
大鸟 男 31
工作经历:2003-2020 ZZ公司
大鸟 男 28
工作经历:2003-2020 ZZ公司

可以看出,这样子会导致如果被克隆的类中还存在其他的类的话,就只会将这个引用指向那个对象,实际上没有克隆

super.clone(),这个操作主要是来做一次bitwise copy( binary copy ),即浅拷贝,他会把原对象完整的拷贝过来包括其中的引用。这样会带来问题,如果里面的某个属性是个可变对象,那么原来的对象改变,克隆的对象也跟着改变。所以在调用完super.clone()后,一般还需要重新拷贝可变对象。

但是如果你没有重写clone方法,则无法克隆Error: java: clone() 在 java.lang.Object 中是 protected 访问控制


深拷贝:

那么,如何进行一个深拷贝呢?

比较常用的方案有两种:

  • 序列化(serialization)这个对象,再反序列化回来,就可以得到这个新的对象,无非就是序列化的规则需要我们自己来写。

  • 继续利用 clone() 方法,既然 clone() 方法,是我们来重写的,实际上我们可以对其内的引用类型的变量,再进行一次 clone()。
    继续改写上面的 Demo ,让 ChildClass 也实现 Cloneable 接口。

    需要将代码改为如下即可:

    public class Resume implements Cloneable {
        
        @Override
        protected Object clone() throws CloneNotSupportedException {
            Resume r = (Resume)super.clone();
            r.we = (WorkExperience)this.we.clone();
            return r;
        }
    }
    
    class WorkExperience implements Cloneable {
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    重写两个方法的clone后,结果就可以进行深拷贝了:

    大鸟 男 28
    工作经历:1998-2000 XX公司
    大鸟 男 31
    工作经历:2000-2003 YY企业
    大鸟 男 28
    工作经历:2003-2020 ZZ公司
    

总结:

每层clone()都顺着 super.clone() 的链向上调用的话最终就会来到Object.clone() ,于是根据上述的特殊语义就可以有 x.clone.getClass() == x.getClass() 。

至于如何实现的,可以把JVM原生实现的Object.clone()的语义想象成拿到this引用后通过反射去找到该对象实例的所有字段,然后逐一字段拷贝。

HotSpot vm中,Object.clone()在不同的优化层级上有不同的实现。在其中最不优化的版本是这样做的:拿到this引用,通过对象头里记录的Class信息去找出这个对象有多大,然后直接分配一个新的同样大的空对象并且把Class信息塞进对象头(这样就已经实现了x.clone.getClass() == x.getClass()这部分语义),然后直接把对象体 的内容看作数组拷贝一样从源对象“盲”拷贝到目标对象,bitwise copy。

我的理解是super.clone() 的调用就是沿着继承树不断网上递归调用直到Object 的clone方法,而跟据JavaDoc所说Object.clone()根据当前对象的类型创建一个新的同类型的空对象,然后把当前对象的字段的值逐个拷贝到新对象上,然后返回给上一层clone() 调用。
也就是说super.clone() 的浅复制效果是通过Object.clone()实现的。

推荐阅读