当前位置:   article > 正文

Java-基础篇-29-【深拷贝和浅拷贝的理解】_java深拷贝 浅拷贝 赋值

java深拷贝 浅拷贝 赋值

1. 概述

深拷贝和浅拷贝主要是针对对象的属性是对象(引用类型)
所谓拷贝,就是赋值。把一个变量赋给另外一个变量,就是把变量的内容进行拷贝。
把一个对象的值赋给另外一个对象,就是把一个对象拷贝一份。

2. 深拷贝和浅拷贝—基本类型

因为,基本类型赋值时,赋的是数据(所以,不存在深拷贝和浅拷贝的问题)

3. 深拷贝和浅拷贝—引用类型

3.1 平时常写的代码

public class Student {
    private String stuName;
    private int age;


    public Student(String stuName, int age) {
        this.stuName = stuName;
        this.age = age;
    }
.......省略
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

测试一下

public class Test1 {
    public static void main(String[] args) {

        Student student = new Student("原来的", 1);

        Student student1 = student;

        student1.setStuName("改变之后的");
        student1.setAge(2);


        System.out.println("student = " + student);
        System.out.println("student1 = " + student1);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在这里插入图片描述

3.2 浅拷贝

对于引用类型,浅拷贝就是拷贝其内存地址;
如果原型对象的成员变量是值类型,将复制一份给克隆对象,也就是说在堆中拥有独立的空间;
如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

要实现对象浅拷贝还是比较简单的,只需要被复制类需要实现 Cloneable 接口,重写 clone 方法即可,注意: clone方法必须需要用public来修饰

对 person 类进行改造如下,使其可以支持浅拷贝。

public class Student implements Cloneable{
    private String stuName;
    private int age;


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

    public Student(String stuName, int age) {
        this.stuName = stuName;
        this.age = age;
    }
........get和set方法此处省略
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

测试一下:

public class StudentTest {

    public static void main(String[] args) throws CloneNotSupportedException {


        Student student = new Student("张三", 1);

        Student student1 = (Student) student.clone();

        student1.setStuName("李四");
        student1.setAge(2);

        System.out.println("student = " + student);
        System.out.println("student1 = " + student1);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述
why?不是说浅拷贝对于引用类型只是指向堆内存地址,两者的结果集应该名字一样才对啊
原因是:
String、Integer 等包装类都是不可变的对象,当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值,然后将原来的引用指向新的地址,所以在这里我们修改了 student1对象的 name 属性值,student1对象的 name 字段指向了内存中新的 name 对象,但是我们并没有改变 student对象的 name 字段的指向,所以 student对象的 name 还是指向内存中原来的 name 地址,也就没有变化,age是int 类型,拷贝的是内容,所以两者不一致;
这种引用是一种特列,因为这些引用具有不可变性

上面因为String是特殊的引用类型,索引我们把String换成Teacher类

public class Teacher {
    private String name;

    public Teacher() {

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    .....省略
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

新增teacher属性类

public class Student implements Cloneable {
    private String stuName;
    private int age;
    private Teacher teacher;


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

    public Teacher getTeacher() {
        return teacher;
    }

    public void setTeacher(String name) {
        this.teacher.setName(name);
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

测试

public class StudentTest {

    public static void main(String[] args) throws CloneNotSupportedException {

        Teacher teacher1 = new Teacher();
        teacher1.setName("老师1号");

        Student student1 = new Student("张三", 1,teacher1);

        Student student2 = (Student) student1.clone();



        student2.setStuName("李四");
        student2.setAge(2);
        student2.setTeacher("老师2号");

        System.out.println("student1 = " + student1);
        System.out.println("student2 = " + student2);


    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在这里插入图片描述
我们修改 student1 的 name 字段之后,student的 name 也发生了改变,这说明 student对象和 student1 对象指向是同一个 teacher对象地址,这也符合浅拷贝引用对象只拷贝引用地址并未创建新对象的定义

3.2 深拷贝

深拷贝也是对象克隆的一种方式,相对于浅拷贝,「深拷贝是一种完全拷贝,无论是值类型还是引用类型都会完完全全的拷贝一份,在内存中生成一个新的对象」,简单点说就是拷贝对象和被拷贝对象没有任何关系,互不影响

深拷贝有两种方式,一种是跟浅拷贝一样实现 Cloneable 接口,另一种是实现 Serializable 接口,用序列化的方式来实现深拷贝,我们分别用这两种方式来实现深拷贝

3.2.1 实现 Cloneable 接口方式

实现 Cloneable 接口的方式跟浅拷贝相差不大,区别在于,浅拷贝对象(比如上面Student)的成员引用对象属性(比如上面的Teacher)不需要实现Cloneable接口,而深拷贝需要引用对象属性(比如Teacher)实现Cloneable接口

Teacher改变: 多实现一个Cloneable接口且重写父类Clone方法

public class Teacher implements Cloneable {
    private String name;

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Student改变: clone方法内重写,加入teacher的clone

public class Student implements Cloneable {
    private String stuName;
    private int age;
    private Teacher teacher;


    @Override
    protected Object clone() throws CloneNotSupportedException {

        Student student = (Student) super.clone();

        student.teacher = (Teacher) teacher.clone();

        return student;

    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

测试方法还是上面的,只是结果不一样了
在这里插入图片描述

3.2.2 实现 Serializable 接口方式(串行化深拷贝)

实现 Serializable 接口方式也可以实现深拷贝,而且这种方式还可以解决多层克隆的问题,多层克隆就是引用类型里面又有引用类型,层层嵌套下去,用 Cloneable 方式实现还是比较麻烦的,一不小心写错了就不能实现深拷贝了,使用 Serializable 序列化的方式就需要所有的对象对实现 Serializable 接口

Teacher类,只需要实现Serializable 接口即可

public class Teacher implements Serializable {

    private static final long serialVersionUID = 369285298572941L;
    
    private String name;

    
    public String getName() {
        return name;
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Student类,其实就是把Cloneable接口换成了Serializable接口,然后新增一个使用流的方法

public class Student implements Serializable {

    private static final long serialVersionUID = 369285298572941L;

    private String stuName;
    private int age;
    private Teacher teacher;
   //自定义使用流的方法
   public Object deepClone() throws IOException, ClassNotFoundException {
        //写入对象
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);
        //读取对象
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

测试方法

public class StudentTest {

    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {

        Teacher teacher1 = new Teacher();
        teacher1.setName("老师1号");

        Student student1 = new Student("张三", 1,teacher1);

        Student student2 = (Student) student1.deepClone();



        student2.setStuName("李四");
        student2.setAge(2);
        student2.setTeacher("老师2号");

        System.out.println("student1 = " + student1);
        System.out.println("student2 = " + student2);


    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

结果:
在这里插入图片描述

4 小结:

实现浅拷贝:

只需要被复制类需要实现 Cloneable 接口,重写 clone 方法即可,注意: clone方法必须需要用public来修饰;

实现深拷贝: 实现 Cloneable 接口方式 和 实现 Serializable 接口方式

Cloneable 接口方式:

实现 Cloneable 接口的方式跟浅拷贝相差不大,区别在于,浅拷贝对象(比如上面Student)的成员引用对象属性(比如上面的Teacher)不需要实现Cloneable接口,而深拷贝需要引用对象属性(比如Teacher)实现Cloneable接口,且重写父类的clone方法内,要加上属性的clone出来的对象引用;

实现 Serializable 接口方式:
实现 Serializable 接口方式也可以实现深拷贝,而且这种方式还可以解决多层克隆的问题,多层克隆就是引用类型里面又有引用类型,层层嵌套下去,用 Cloneable 方式实现还是比较麻烦的,一不小心写错了就不能实现深拷贝了,使用 Serializable 序列化的方式就需要所有的对象对实现 Serializable 接口

只需要被深拷贝的对象和深拷贝对象的引用对象属性实现Serializable接口即可,克隆方法自己取名字用流写就行了

Object 为什么 protected修饰的clone方法?

在java.lang.Object的中,他将clone方法设置为protected修饰,这是很特殊的一种情况。protected的作用域是:包可见+可继承。之所以这样设置,是因为这个方法要返回的是克隆出来的对象,即clone方法要去克隆的类型是未知的,没有办法确定返回值的类型,自然只能让子孙后代来实现它重写它,为了能够让后代继承而又不过与张开,设置为了protected类型。

把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做 “解冻”或者“回鲜(depicking)”过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。
上面是网上的专业解释,我也不在这里班门弄斧了。在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。

public Object deepClone() 
{ 
 //写入对象 
 ByteArrayOutoutStream bo=new ByteArrayOutputStream(); 
 ObjectOutputStream oo=new ObjectOutputStream(bo); 
 oo.writeObject(this); 
 //读取对象
 ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray()); 
 ObjectInputStream oi=new ObjectInputStream(bi); 
 return(oi.readObject()); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

虽然这种学院派的代码看起来很复杂,其实只是把对象放到流里,再拿出来。相比较分析判断无数的clone,这样简直是再简单不过了。这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象是否设成transient。

transient:一个对象只要实现了Serilizable接口,这个对象就可以被序列化(序列化是指将java代码以字节序列的形式写出,即我们上面代码前三行写入对象),Java的这种序列化模式为开发者提供了很多便利,可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个的所有属性和方法都会自动序列化。但是有种情况是有些属性是不需要序列号的,所以就用到这个关键字。只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/616663
推荐阅读
相关标签
  

闽ICP备14008679号