美文网首页
java之深拷贝与浅拷贝

java之深拷贝与浅拷贝

作者: Sophie12138 | 来源:发表于2018-08-08 19:38 被阅读5次

    拷贝的定义

    和new对象过程相似,拷贝(clone)后是独立的对象,与引用的复制或者称为引用的拷贝区分开来。

    引用的拷贝

    在开始前先看看引用的拷贝,针对的是同一个对象。

    //引用拷贝
    private static void copyReferenceObject() {
        Person p = new Person(23, "zhang");
        Person p1 = p;
        System.out.println(p);
        System.out.println(p1);
     }
    

    这里打印的结果:
    com.yaolong.clone.Person@3654919e
    com.yaolong.clone.Person@3654919e


    image.png

    可以看到,打印的结果是一样的,也就是说,二者的引用是同一个对象,并没有创建出一个新的对象。因此要区分引用拷贝和对象拷贝的区别。

    而在对象的拷贝过程中,对于对象中的引用是否拷贝,分为浅拷贝和深拷贝。

    浅拷贝

    浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

    实现对象拷贝的类,必须实现Cloneable接口,并覆写clone()方法。

    Persion类:
    public class Person implements Cloneable {
        private int age;
        private String name;
    
        public Person(Integer age, String name) {
            super();
            this.age = age;
            this.name = name;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    需要注意的是,clone()方法本来就是Object缺省继承于Object类的,为什么这里还要实现Cloneable 接口呢?其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对 Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了 super.Clone()方法),那么Object的clone()方法就会抛出CloneNotSupportedException异常。

    再看,clone()方法其实是一个native方法,本地方法,直接调用执行,自然快。

    //对象拷贝
    private static void copyRealObject() throws CloneNotSupportedException{
        Person p = new Person(23, "zhang");
        Person p1 = (Person) p.clone();      
        System.out.println(p);
        System.out.println(p1);
    }
    

    这里打印的结果:
    com.yaolong.clone.Person@28084850
    com.yaolong.clone.Person@37c390b8

    可以看出,二者的对象地址不一样,因此实现了拷贝。

    image.png

    但是还是有个问题,就是Person类中有一个String类型的引用对象name,它真的也被拷贝过去了吗,还是说依然是引用的是同一个name对象呢,在上面的代码基础上,我们继续打印:

        System.out.println("pName:"+p.getName().hashCode());
        System.out.println("p1Name:"+p1.getName().hashCode());
    

    这里打印的结果:
    pName:115864556
    p1Name:115864556

    可见,二者的name属性依然是指向同一个对象。基本数据类型是不存在引用问题。

    这实际上就是典型的浅拷贝:

    image.png

    由上可知,从Object中继承过来的clone默认实现的是浅拷贝。

    深拷贝

    深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

    如上面的浅拷贝问题深拷贝的内存图:

    image.png

    接着分析在拷贝对象的引用的对象中还存在引用,深拷贝是如何进行的。

    class Body implements Cloneable{
        public Head head;
        public Body() {}
        public Body(Head head) { 
    this.head = head;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            Body newBody =  (Body) super.clone();
            newBody.head = (Head) head.clone();
            return newBody;
        }
    }
    
    
    class Head implements Cloneable{
        public  Face face;
        public Head() {}
        public Head(Face face){this.face = face;}
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    } 
    public static void main(String[] args) throws CloneNotSupportedException {
    
        Body body = new Body(new Head());
        Body body1 = (Body) body.clone();
        System.out.println("body == body1 : " + (body == body1) );
        System.out.println("body.head == body1.head : " +  (body.head == body1.head));
    }
    

    打印结果为:
    body == body1 : false
    body.head == body1.head : false

    由此可见, body和body1内的head引用指向了不同的Head对象, 也就是说在clone Body对象的同时, 也拷贝了它所引用的Head对象, 进行了深拷贝。

    但真的是深拷贝吗

    由上一节的内容可以得出如下结论:如果想要深拷贝一个对象, 这个对象必须要实现Cloneable接口,实现clone方法,并且在clone方法内部,把该对象引用的其他对象也要clone一份 , 这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。

    那么,按照上面的结论, Body类组合了Head类, 而Head类组合了Face类,要想深拷贝Body类,必须在Body类的clone方法中将Head类也要拷贝一份,但是在拷贝Head类时,默认执行的是浅拷贝,也就是说Head中组合的Face对象并不会被拷贝。验证代码如下:(这里本来只给出Face类的代码就可以了, 但是为了阅读起来具有连贯性,避免丢失上下文信息, 还是给出整个程序,整个程序也非常简短)

    class Body implements Cloneable{
        public Head head;
        public Body() {}
        public Body(Head head) {this.head = head;}
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            Body newBody =  (Body) super.clone();
            newBody.head = (Head) head.clone();
            return newBody;
        }
    
    }
    
    class Head implements Cloneable{
        public  Face face;
    
        public Head() {}
        public Head(Face face){this.face = face;}
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    } 
    
    class Face{}
    
    public static void main(String[] args) throws CloneNotSupportedException {
    
        Body body = new Body(new Head(new Face()));
    
        Body body1 = (Body) body.clone();
    
        System.out.println("body == body1 : " + (body == body1) );
    
        System.out.println("body.head == body1.head : " +  (body.head == body1.head));
    
        System.out.println("body.head.face == body1.head.face : " +  (body.head.face == body1.head.face));
    
    }
    

    打印结果为:
    body == body1 : false
    body.head == body1.head : false
    body.head.face == body1.head.face : true

    image.png

    那么,对Body对象来说,算是这算是深拷贝吗?其实应该算是深拷贝,因为对Body对象内所引用的其他对象(目前只有Head)都进行了拷贝,也就是说两个独立的Body对象内的head引用已经指向了独立的两个Head对象。但是,这对于两个Head对象来说,他们指向了同一个Face对象,这就说明,两个Body对象还是有一定的联系,并没有完全的独立。这应该说是一种不彻底的深拷贝。

    image.png

    clone在平时项目的开发中可能用的不是很频繁,但是区分深拷贝和浅拷贝会让我们对java内存结构和运行方式有更深的了解。至于彻底深拷贝,几乎是不可能实现的,依此类推,如果Face对象还引用了其他的对象, 比如说Mouth,如果不经过处理,Body对象拷贝之后还是会通过一级一级的引用,引用到同一个Mouth对象。同理, 如果要让Body在引用链上完全独立, 只能显式的让Mouth对象也被拷贝。

    到此,可以得到如下结论:如果在拷贝一个对象时,要想让这个拷贝的对象和源对象完全彼此独立,那么在引用链上的每一级对象都要被显式的拷贝。所以创建彻底的深拷贝是非常麻烦的,尤其是在引用关系非常复杂的情况下, 或者在引用链的某一级上引用了一个第三方的对象, 而这个对象没有实现clone方法, 那么在它之后的所有引用的对象都是被共享的。

    深拷贝和彻底深拷贝,在创建不可变对象时,可能对程序有着微妙的影响,可能会决定我们创建的不可变对象是不是真的不可变。clone的一个重要的应用也是用于不可变对象的创建。

    相关文章

      网友评论

          本文标题:java之深拷贝与浅拷贝

          本文链接:https://www.haomeiwen.com/subject/wbbxbftx.html