Java的深浅拷贝

深/浅拷贝

浅拷贝

1,对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

2,对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行“引用传递”,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

浅拷贝是使用clone( )方法来实现的

深拷贝

不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。

深拷贝是使用clone( )方法来实现也可以使用对象序列化来实现。

区别

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

实操

(1) 如何实现浅拷贝?

protected native Object clone() throws CloneNotSupportedException;

如果想要使一个类的对象能够调用clone方法 ,则需要实现Cloneable接口, 并重写 clone方法:

public class Student implements Cloneable{


    private int sno ;
    private String name;

    //getter ,setter 省略

    @Override
    public Object clone() throws CloneNotSupportedException {
        Student s = null;
        try{
            s = (Student)super.clone();
        }catch (Exception e){
            e.printStackTrace();
        }
        return s;
    }

}

现在测试clone方法:

	@Test
    public void test04() throws CloneNotSupportedException {
        //创建Student对象
        Student s1 = new Student();
        s1.setSno(1);
        s1.setName("Rye");

        //通过clone 拷贝一个对象
        Student s2 = (Student)s1.clone();

        System.out.println("s1:"+s1);
        System.out.println("s2:"+s2);
        System.out.println("s1 == s2 ? ==> "+(s1 == s2));
    }

按照预期,克隆出的对象s2中的字段值应该与s1相同,但与s1对应的对象不在同一块内存空间,结果如下:

s1:Student{sno=1, name='Rye'}
s2:Student{sno=1, name='Rye'}
s1 == s2 ? ==> false

此时如果修改 s1中的sno为2,那么会不会影响到s2中的sno呢?

//修改s1中的sno
s1.setSno(2);

结果如下:

s1:Student{sno=2, name='Rye'}
s2:Student{sno=1, name='Rye'}

此时看似已经完成了 copy, s1 与 s2有着自己不同的值,但如果为Student中新增了Teacher类型的成员变量,结果还是跟上面一样吗?让我们改造下代码:

public class Teacher {

    private int tno;
    private String name;
  
    //getter setter省略...  
}
public class Student  implements Cloneable{

    private int sno ;
    private String name;
    private Teacher teacher;

    //getter ,setter ,toString 省略...

    @Override
    public Object clone() throws CloneNotSupportedException {
        Student s = null;
        try{
            s = (Student)super.clone();
        }catch (Exception e){
            e.printStackTrace();
        }
        return s;
    }    
}

此时测试代码如下:

@Test
    public void test02() throws CloneNotSupportedException {
        Student student1 = new Student();
        student1.setSno(1);
        student1.setName("Rye");

        Teacher teacher = new Teacher();
        teacher.setTno(1);
        teacher.setName("LinTong");

        student1.setTeacher(teacher);

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

        System.out.println("student1:"+student1);
        System.out.println("student2:"+student2);
        System.out.println("student1 == student2 ? ==> "+ (student1 ==student2));
        System.out.println("student1.teacher == student2.teacher ? ==> "+ (student1.getTeacher() ==student2.getTeacher()));

    }

运行结果如下:

student1:Student{sno=1, name='Rye', teacher=Teacher{tno=1, name='LinTong'}}
student2:Student{sno=1, name='Rye', teacher=Teacher{tno=1, name='LinTong'}}
student1 == student2 ? ==> false
student1.teacher == student2.teacher ? ==> true

由此可见,此时经过clone生成的student2, 与 student1.二者中的teacher字段, 指向同一块内存空间;

那么可能会有人问,这会有什么影响吗?

我们拷贝的目的,更多的时候是希望获得全新并且值相同的对象,操作原始对象或拷贝的新对象,对彼此之间互不影响;

此时我们修改student1中teacher的tno ,如下:

//修改teacher中的 tno值为2
student1.getTeacher().setTno(2);

再次运行test:

student1:Student{sno=1, name='Rye', teacher=Teacher{tno=2, name='LinTong'}}
student2:Student{sno=1, name='Rye', teacher=Teacher{tno=2, name='LinTong'}}
student1 == student2 ? ==> false
student1.teacher == student2.teacher ? ==> true

此时发现,student2中的teacher的tno ,也跟着变化了.

变化的原因是:通过student1执行clone时,基本类型会完全copy一份到student2对应对象内存空间中, 但是对于Teacher对象仅仅是copy了一份Teacher的引用而已.

而student1 与 student2的引用 指向的是同一块堆内存,因此不论是通过student1或是student2修改teacher 都会影响另外一个;

通过图会更直观一些:
在这里插入图片描述

2.浅拷贝中引用类型的变量拷贝的是对象的引用 , 可通过如下思路解决:

Teacher类中也覆写clone方法:

@Override
    protected Object clone() {
        Teacher teacher = null;
        try {
            teacher = (Teacher)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return teacher;
    }

修改Student中的clone方法,如下:

@Override
    public Object clone() {
        Student s = null;
        try{
            s = (Student)super.clone();
            Teacher t = (Teacher)this.teacher.clone();
            s.setTeacher(t);
        }catch (Exception e){
            e.printStackTrace();
        }
        return s;
    }

此时再次运行test:

student1:Student{sno=1, name='Rye', teacher=Teacher{tno=2, name='LinTong'}}
student2:Student{sno=1, name='Rye', teacher=Teacher{tno=1, name='LinTong'}}
student1 == student2 ? ==> false
student1.teacher == student2.teacher ? ==> false

由此可见,在copy Student的同时 将Teacher也进行了修改,如图:

在这里插入图片描述

目前来看是满足了我们的需求,但是如果Teacher类中,同样也有别的引用类型 的成员变量呢?

那么就同样需要一直覆写clone方法,如果这个关系不是特多还可以接受,如果引用关系很复杂就会显得代码繁琐;

此时应该使用序列化完成深度拷贝;

(2) 如何实现深拷贝?

使用序列化完成深拷贝

深拷贝是利用对象流,将对象序列化,再反序列化得出新的对象. 因此首先需要实现序列化接口,如下:

public class Student implements Serializable{

    private static final long serialVersionUID = -2232725257771333130L;

    private int sno ;
    private String name;
    private Teacher teacher;  //getter ,setter,toString()省略...
}

Teacher也要实现序列化接口:

public class Teacher implements Serializable{
    private static final long serialVersionUID = 4477679176385287943L;
    private int tno;
    private String name;  
 //getter ,setter,toString()省略...
}

工具方法:

//工具方法
  public Object cloneObject(Object object) throws IOException, ClassNotFoundException {
        //将对象序列化
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(object);

        //将字节反序列化
        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        Object obj = objectInputStream.readObject();

        return obj;
    }

测试类:

public void test05() throws IOException, ClassNotFoundException {
        Student student1 = new Student();
        student1.setSno(1);
        student1.setName("Rye");

        Teacher teacher = new Teacher();
        teacher.setTno(1);
        teacher.setName("LinTong");

        student1.setTeacher(teacher);

        Student student2 = (Student)cloneObject(student1);
        //修改teacher中的 tno值为2
        student1.getTeacher().setTno(2);

        System.out.println("student1:"+student1);
        System.out.println("student2:"+student2);
        System.out.println("student1 == student2 ? ==> "+ (student1 ==student2));
        System.out.println("student1.teacher == student2.teacher ? ==> "+ (student1.getTeacher() ==student2.getTeacher()));
    }

如果Teacher类或者Student类没有实现序列化接口,则执行时会报异常,如下:

java.io.NotSerializableException: com.example.test.Teacher

在都实现了Serializable接口的情况下,运行结果如下:

student1:Student{sno=1, name='Rye', teacher=Teacher{tno=2, name='LinTong'}}
student2:Student{sno=1, name='Rye', teacher=Teacher{tno=1, name='LinTong'}}
student1 == student2 ? ==> false
student1.teacher == student2.teacher ? ==> false

由此通过对象流的方式,成功完成了深度拷贝;

小总结

clone方法:

优点:速度快,效率高

缺点:在对象引用比较深时,使用此方式比较繁琐

通过序列化:

优点:非常简便的就可以完成深度copy

缺点:由于序列化的过程需要跟磁盘打交道,因此效率会低于clone方式

如何抉择?

实际开发中,根据两种方式的优缺点进行选择即可!

猜你喜欢

转载自blog.csdn.net/sgx5666666/article/details/108218759
今日推荐