是时候捋一捋 Java 的深浅拷贝了

在开发、刷题、面试中,咱们可能会遇到将一个对象的属性赋值到另外一个对象的状况,这种状况就叫作拷贝。拷贝与Java内存结构息息相关,搞懂Java深浅拷贝是很必要的!程序员

在对象的拷贝中,不少初学者可能搞不清究竟是拷贝了引用仍是拷贝了对象。在拷贝中这里就分为引用拷贝、浅拷贝、深拷贝进行讲述。面试

引用拷贝

引用拷贝会生成一个新的对象引用地址,可是两个最终指向依然是同一个对象。如何更好的理解引用拷贝呢?很简单,就拿咱们人来讲,一般有个姓名,可是不一样场合、人物对咱们的叫法可能不一样,但咱们很清楚哪些名称都是属于"我"的!算法

 

固然,经过一个代码示例让你们领略一下(为了简便就不写get、set等方法):json

class Son {
    String name;
    int age;

    public Son(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class test {
    public static void main(String[] args) {
        Son s1 = new Son("son1"12);
        Son s2 = s1;
        s1.age = 22;
        System.out.println(s1);
        System.out.println(s2);
        System.out.println("s1的age:" + s1.age);
        System.out.println("s2的age:" + s2.age);
        System.out.println("s1==s2" + (s1 == s2));//相等
    }
}

输出的结果为:数组

Son@135fbaa4
Son@135fbaa4
s1的age:22
s2的age:22
true

浅拷贝

如何建立一个对象,将目标对象的内容复制过来而不是直接拷贝引用呢?缓存

这里先讲一下浅拷贝,浅拷贝会建立一个新对象,新对象和原对象自己没有任何关系,新对象和原对象不等,可是新对象的属性和老对象相同。具体能够看以下区别:微信

  • 若是属性是基本类型(int,double,long,boolean等),拷贝的就是基本类型的值;ide

  • 若是属性是引用类型,拷贝的就是内存地址(即复制引用但不复制引用的对象) ,所以若是其中一个对象改变了这个地址,就会影响到另外一个对象。函数

若是用一张图来描述一下浅拷贝,它应该是这样的:this

 

如何实现浅拷贝呢?也很简单,就是在须要拷贝的类上实现Cloneable接口并重写其clone()方法

@Override
protected Object clone() throws CloneNotSupportedException {
  return super.clone();
}

在使用的时候直接调用类的clone()方法便可。具体案例以下:

class Father{
    String name;
    public Father(String name) {
        this.name=name;
    }
    @Override
    public String toString() {
        return "Father{" +
                "name='" + name + '\'' +
                '}';
    }
}
class Son implements Cloneable {
    int age;
    String name;
    Father father;
    public Son(String name,int age) {
        this.age=age;
        this.name = name;
    }
    public Son(String name,int age, Father father) {
        this.age=age;
        this.name = name;
        this.father = father;
    }
    @Override
    public String toString() {
        return "Son{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", father=" + father +
                '}';
    }
    @Override
    protected Son clone() throws CloneNotSupportedException {
        return (Son) super.clone();
    }
}
public class test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Father f=new Father("bigFather");
        Son s1 = new Son("son1",13);
        s1.father=f;
        Son s2 = s1.clone();

        System.out.println(s1);
        System.out.println(s2);
        System.out.println("s1==s2:"+(s1 == s2));//不相等
        System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//相等
        System.out.println();

        //可是他们的Father father 和String name的引用同样
        s1.age=12;
        s1.father.name="smallFather";//s1.father引用未变
        s1.name="son222";//相似 s1.name=new String("son222") 引用发生变化
        System.out.println("s1.Father==s2.Father:"+(s1.father == s2.father));//相等
        System.out.println("s1.name==s2.name:"+(s1.name == s2.name));//不相等
        System.out.println(s1);
        System.out.println(s2);
    }
}

运行结果为:

Son{age=13, name='son1', father=Father{name='bigFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}
s1==s2:false
s1.name==s2.name:true//此时相等

s1.Father==s2.Father:true
s1.name==s2.name:false//修改引用后不等
Son{age=12, name='son222', father=Father{name='smallFather'}}
Son{age=13, name='son1', father=Father{name='smallFather'}}

不出意外,这种浅拷贝除了对象自己不一样之外,各个零部件和关系和拷贝对象都是相同的,就好像双胞胎同样,是两我的,可是其开始的样貌、各类关系(父母亲人)都是相同的。须要注意的是其中name初始==是相等的,是由于初始浅拷贝它们指向一个相同的String,然后s1.name="son222" 则改变引用指向。

 

深拷贝

对于上述的问题虽然拷贝的两个对象不一样,但其内部的一些引用仍是相同的,怎么样绝对的拷贝这个对象,使这个对象彻底独立于原对象呢?就使用咱们的深拷贝了。深拷贝:在对引用数据类型进行拷贝的时候,建立了一个新的对象,而且复制其内的成员变量。

 

在具体实现深拷贝上,这里提供两个方式,重写clone()方法和序列法。

重写clone()方法

若是使用重写clone()方法实现深拷贝,那么要将类中全部自定义引用变量的类也去实现Cloneable接口实现clone()方法。对于字符类能够建立一个新的字符串实现拷贝。

对于上述代码,Father类实现Cloneable接口并重写clone()方法。son的clone()方法须要对各个引用都拷贝一遍

//Father clone()方法
@Override
protected Father clone() throws CloneNotSupportedException {
    return (Father) super.clone();
}
//Son clone()方法
@Override
protected Son clone() throws CloneNotSupportedException {
    Son son= (Son) super.clone();//待返回拷贝的对象
    son.name=new String(name);
    son.father=father.clone();
    return son;
}

其余代码不变,执行结果以下:

Son{age=13, name='son1', father=Father{name='bigFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}
s1==s2:false
s1.name==s2.name:false

s1.Father==s2.Father:false
s1.name==s2.name:false
Son{age=12, name='son222', father=Father{name='smallFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}

序列化

能够发现这种方式实现了深拷贝。可是这种状况有个问题,若是引用数量或者层数太多了怎么办呢?

 

不可能去每一个对象挨个写clone()吧?那怎么办呢?借助序列化啊。

由于序列化后:将二进制字节流内容写到一个媒介(文本或字节数组),而后是从这个媒介读取数据,原对象写入这个媒介后拷贝给clone对象,原对象的修改不会影响clone对象,由于clone对象是从这个媒介读取。

熟悉对象缓存的知道咱们常常将Java对象缓存到Redis中,而后还可能从Redis中读取生成Java对象,这就用到序列化和反序列化。通常能够将Java对象存储为字节流或者json串而后反序列化成Java对象。由于序列化会储存对象的属性可是不会也没法存储对象在内存中地址相关信息。因此在反序列化成Java对象时候会从新建立全部的引用对象。

在具体实现上,自定义的类须要实现Serializable接口。在须要深拷贝的类(Son)中定义一个函数返回该类对象:

protected Son deepClone() throws IOException, ClassNotFoundException {
      Son son=null;
      //在内存中建立一个字节数组缓冲区,全部发送到输出流的数据保存在该字节数组中
      //默认建立一个大小为32的缓冲区
      ByteArrayOutputStream byOut=new ByteArrayOutputStream();
      //对象的序列化输出
      ObjectOutputStream outputStream=new ObjectOutputStream(byOut);//经过字节数组的方式进行传输
      outputStream.writeObject(this);  //将当前student对象写入字节数组中

      //在内存中建立一个字节数组缓冲区,从输入流读取的数据保存在该字节数组缓冲区
      ByteArrayInputStream byIn=new ByteArrayInputStream(byOut.toByteArray()); //接收字节数组做为参数进行建立
      ObjectInputStream inputStream=new ObjectInputStream(byIn);
      son=(Son) inputStream.readObject(); //从字节数组中读取
      return  son;
}

使用时候调用咱们写的方法便可,其余不变,实现的效果为:

Son{age=13, name='son1', father=Father{name='bigFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}
s1==s2:false
s1.name==s2.name:false

s1.Father==s2.Father:false
s1.name==s2.name:false
Son{age=12, name='son222', father=Father{name='smallFather'}}
Son{age=13, name='son1', father=Father{name='bigFather'}}

 

固然这是对象的拷贝,对于数组的拷贝将在下一篇进行更细致的研究!敬请期待!

推荐阅读

  面试官本想拿一道求素数搞我,但被我优雅的"回击"了

 16张图带你完全搞懂基数排序

「干货总结」程序员必知必会的十大排序算法

  花5分钟看这篇以前,你才发现你不懂RESTful

「五大经常使用算法」一文图解分治算法和思想

记得关注、我们下次再见!

本文分享自微信公众号 - bigsai(bigsai)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索