Java编程思想阅读感悟深拷贝与浅拷贝

Posted 穿黑风衣的牛奶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java编程思想阅读感悟深拷贝与浅拷贝相关的知识,希望对你有一定的参考价值。

深拷贝和浅拷贝

一:解释定义

所谓拷贝,顾名思义就是将一个对象的属性复制到复制给另一个对象,但是在Java中,拷贝并不像字面意义上的这么简单,在Java中拷贝分为深拷贝与浅拷贝。在开始,我们先定义一下,被克隆的对象称为原对象,克隆后的对象称为新对象,对于一个对象中定义了其他对象,以此类推的,我们将其称为对象链

  • 浅拷贝:浅拷贝就是对于基本类型进行值的复制,对于引用类型则将这个引用类型的地址复制一份给新对象,所以深拷贝之后,对于原对象的引用类型的更改,会对新对象有影响,反之亦然。

  • 深拷贝:深拷贝就是将对象中所有的属性,以及对象中的对象都复制一份新的给克隆出来的对象,对于原对象中的基本类型,那就直接将基本类型的值复制一份给新对象,对于原对象中的引用类型,那就对引用类型所有的成员进行开辟内存空间的完全复制,而不是仅仅只是复制一个引用,如果引用对象中还有其他的对象,即对对象链中的所有对象都进行完全复制。因此深拷贝完之后,原对象中新对象和原对象完全是两个不同的对象,所有的地址都不一样,因此对于原对象中的任何更改,都不会对新对象有任何影响,反之亦然。

    简单地说,深拷贝对引用数据类型的成员变量的对象链中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。

二、画图理解

浅拷贝:由图可见浅拷贝对于引用类型只是简单的把新对象直接指向原对象的引用类型对应的地址。所以不管是谁改obj都会对另一个对象的obj有影响

深拷贝:由图可见深拷贝是每一个对象都是完全复制,并且他们的地址都是不同的,所以对原对象或者新对象的更改都不会相互影响

三、实现方式

3.1.浅拷贝的实现方式

3.1.1.构造方法拷贝

因为Java中对对象的赋值操作,也是类同于将一个原对象的引用复制给另一个对象,所以使用构造方法实际上也可以实现浅拷贝

/**
 * 通过构造方法进行浅拷贝
 */
@Data
class Dog implements Cloneable{

    private String name;
    private  int age;
    private Foot foot;

    public Dog(Dog dog){
        this.age=dog.age;
        this.name=dog.name;
        this.foot=dog.foot;
    }
    public Dog(String name, int age,Foot foot){
        this.age=age;
        this.name=name;
        this.foot=foot;
    }
    @Override
    public String toString() {
        return "Dog{" +
                "name=\'" + name + \'\\\'\' +
                ", age=" + age +",address="+System.identityHashCode(this)+
                ", foot=" + foot +
                \'}\';
    }
}
@Data
class Foot {
    private String footNum;
    public Foot(String footNum) {
        this.footNum = footNum;
    }

    @Override
    public String toString() {
        return "Foot{" +
                "footNum=" +footNum +",address="+System.identityHashCode(this) +","+
                \'}\';
    }
}
public class ShallowCopy {
    public static void main(String[] args) {
        Foot foot = new Foot("四只脚");
        Dog dog1 = new Dog("旺财", 12, foot);
        Dog dog2 = new Dog(dog1);
        System.out.println("原对象---"+dog1);
        System.out.println("克隆后---"+dog2);
        dog1.setAge(18);
       	foot.setFootNum("六只脚");
        System.out.println("修改后的原对象---"+dog1);
        System.out.println("修改后克隆对象---"+dog2);

    }

}

输出:

原对象---Dog{name=\'旺财\', age=12,address=1406718218, foot=Foot{footNum=四只脚,address=245257410,}}
克隆后---Dog{name=\'旺财\', age=12,address=1705736037, foot=Foot{footNum=四只脚,address=245257410,}}
修改后的原对象---Dog{name=\'旺财\', age=18,address=1406718218, foot=Foot{footNum=六只脚,address=245257410,}}
修改后克隆对象---Dog{name=\'旺财\', age=12,address=1705736037, foot=Foot{footNum=六只脚,address=245257410,}}

可以看到,dog1和dog2中的foot对象的地址值都是一样的,并且对dog1中的foot的更改会对dog2产生影响

3.1.2.重写clone方法

通过实现Cloneable接口,并且重写Object类中的clone方法来实现浅拷贝,这里要注意的是,要通过clone方法来实现浅拷贝的话,必须要实现Cloneable接口,否则的话将会抛一个CloneNotSupportedException异常。再来谈谈这个Cloneable接口,点进去之后可以发现里面的代码是这样的

public interface Cloneable {
}

没错,Cloneable里面什么都没有,实际上Cloneable起的只是一个标识作用,他的作用就是用于标识某个类是可以被克隆的,如果没有实现这个接口,就代表该类不支持克隆。回过头来看如何用clone方法进行浅拷贝。为方便此处的Foot用的是上面代码的Foot

/**
 * 通过clone方法进行浅拷贝
 */
@Data
class Pig implements Cloneable{

    private String name;
    private  int age;
    private Foot foot;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public String toString() {
        return "Pig{" +
                "name=\'" + name + \'\\\'\' +
                ", age=" + age +",address="+System.identityHashCode(this)+
                ", foot=" + foot +
                \'}\';
    }
}
public class ShallowCopyClone {
    public static void main(String[] args) throws CloneNotSupportedException {
        Foot foot = new Foot("四只脚");
        Pig pig = new Pig();
        pig.setAge(18);
        pig.setFoot(foot);
        pig.setName("猪八戒");
        Pig clonePig = (Pig) pig.clone();
        System.out.println("原对象--"+pig);
        System.out.println("克隆后--"+clonePig);
        //现在改变pig中的属性
        pig.setAge(20);
        clonePig.setAge(80);
        pig.getFoot().setFootNum("一百只脚");
        System.out.println("修改后的原对象---"+pig);
        System.out.println("修改后克隆对象---"+clonePig);

    }
}


输出:

原对象--Pig{name=\'猪八戒\', age=18,address=1406718218, foot=Foot{footNum=四只脚,address=245257410,}}
克隆后--Pig{name=\'猪八戒\', age=18,address=1705736037, foot=Foot{footNum=四只脚,address=245257410,}}
修改后的原对象---Pig{name=\'猪八戒\', age=20,address=1406718218, foot=Foot{footNum=一百只脚,address=245257410,}}
修改后克隆对象---Pig{name=\'猪八戒\', age=80,address=1705736037, foot=Foot{footNum=一百只脚,address=245257410,}}

从输出中可以看到,结果和上面的一样,foot的hashcode地址都是一样的,并且对任意对象中引用对象的改变都会影响clone的其他的对象中的属性

3.2.深拷贝的实现方式

3.2.1.重写所有clone

思路就是为对象链的每一个对象都进行克隆,然后在最顶层的类的克隆方法中调用所有的clone方法,总结就是,对对象链中每一个对象的浅拷贝=深拷贝。代码实现:

/**
 * 通过重写所有的clone方法进行深拷贝
 */
@Data
class Ear implements Cloneable,Serializable{

    private int earNum;
    private String earName;

    @Override
    public String toString() {
        return "Ear{" +
                "earNum=" + earNum +
                ", earName=\'" + earName + \'\\\'\' +",address="+System.identityHashCode(this)+
                \'}\';
    }
    public Ear(int earNum, String earName) {
        this.earNum = earNum;
        this.earName = earName;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
@Data
class Cat implements Cloneable, Serializable {
    private  int age;
    private String name;
    private Ear ear;
    
    public Cat(int age, String name, Ear ear) {
        this.age = age;
        this.name = name;
        this.ear = ear;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Cat cloneCat =(Cat) super.clone();
        //对Ear进行克隆
        cloneCat.ear = (Ear) cloneCat.getEar().clone();
        return cloneCat;
    }
    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                ", name=\'" + name + \'\\\'\' +",address="+System.identityHashCode(this)+
                ", ear=" + ear +
                \'}\';
    }
}
public class DepthCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        Ear ear = new Ear(2, "左耳朵");
        Cat cat = new Cat(18, "加菲猫", ear);
        Cat cloneCat = (Cat)cat.clone();
        System.out.println("原始的"+cat);
        System.out.println("克隆的"+cloneCat);
        //和之前一样改变ear的值
        ear.setEarName("右耳朵");
        ear.setEarNum(1);
        System.out.println("修改后的原对象"+cat);
        System.out.println("修改后克隆对象"+cloneCat);
    }
}

输出:

原始的Cat{age=18, name=\'加菲猫\',address=1406718218, ear=Ear{earNum=2, earName=\'左耳朵\',address=245257410}}
克隆的Cat{age=18, name=\'加菲猫\',address=1705736037, ear=Ear{earNum=2, earName=\'左耳朵\',address=455659002}}
修改后的原对象Cat{age=18, name=\'加菲猫\',address=1406718218, ear=Ear{earNum=1, earName=\'右耳朵\',address=245257410}}
修改后克隆对象Cat{age=18, name=\'加菲猫\',address=1705736037, ear=Ear{earNum=2, earName=\'左耳朵\',address=455659002}}

通过输出可以看出,在深拷贝后,Cat对象的Ear对象的地址值都是不同的,可以说明这是两个毫无关联的对象,因此对于任意Cat对象的更改,都不会对它的克隆对象产生影响。

但是使用这种方式进行深拷贝也有一个明显的缺点,如果某个对象中只引用了一两个对象还好说,只需要对这些对象进行重写clone方法即可实现深拷贝,但是如果某个对象有很多引用对象,而引用对象中又引用了其他的对象,或者引用的对象时不可被修改的,对象链很长的情况下,对对象链都要重写一遍clone方法就显得太过麻烦了。这个时候一般会选择使用序列化进行深拷贝。

3.2.2.序列化

首先先说一下序列化到底是什么,以下是百度的关于序列化的解释

序列化:序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象(Java中被transient修饰的不可被序列化);

我自己理解就是,把一个对象看做是一个大房子,而序列化则是把这个房子中所有的结构,包括每一张桌子(引用对象)怎么摆的,用的什么材料做的,都用图纸记录下来。然后将这个房子一块一块拆下来,通过卡车运到需要的地方,然后通过图纸再搭起来(反序列化)。而对于Java对象来说的话,就是序列化会复制对象中所有的对象的值,比方说A对象age的值为12,然后反序列化之后就会把这些信息读出来,进行对象创建。因此我们就可以通过这种方式来实现深拷贝。

代码实现

/**
 * 通过序列化进行深拷贝
 */
public class DepthCopySerializable{
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        Ear ear = new Ear(2, "左耳");
        Cat cat = new Cat(15, "加菲猫", ear);
        ByteArrayOutputStream BO = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(BO);
        outputStream.writeObject(cat);
        outputStream.flush();
        ObjectInputStream inputStream=new ObjectInputStream(new ByteArrayInputStream(BO.toByteArray()));
        Cat cloneCat = (Cat) inputStream.readObject();
        System.out.println("原始的"+cat);
        System.out.println("克隆后"+cloneCat);
        //改变ear
        ear.setEarNum(6);
        ear.setEarName("右耳朵");
        System.out.println("改变ear后的原对象-----"+cat);
        System.out.println("改变ear后克隆对象-----"+cloneCat);


    }
}

输出

原始的Cat{age=15, name=\'加菲猫\',address=295530567, ear=Ear{earNum=2, earName=\'左耳\',address=2003749087}}
克隆后Cat{age=15, name=\'加菲猫\',address=193064360, ear=Ear{earNum=2, earName=\'左耳\',address=109961541}}
改变ear后的原对象-----Cat{age=15, name=\'加菲猫\',address=295530567, ear=Ear{earNum=6, earName=\'右耳朵\',address=2003749087}}
改变ear后克隆对象-----Cat{age=15, name=\'加菲猫\',address=193064360, ear=Ear{earNum=2, earName=\'左耳\',address=109961541}}

和上面的深拷贝一样,通过序列化实现深拷贝之后的地址都是不同的,说明这是两个完全不同的对象。自然也不会影响了

以上是关于Java编程思想阅读感悟深拷贝与浅拷贝的主要内容,如果未能解决你的问题,请参考以下文章

java 深拷贝与浅拷贝机制详解

Java轻松理解深拷贝与浅拷贝

浅析java的深拷贝与浅拷贝

JavaSE知识集锦深拷贝与浅拷贝

Java深拷贝与浅拷贝

VUE开发 - 深拷贝与浅拷贝探讨