Java高级——深拷贝与浅拷贝的区别

185 阅读5分钟

浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了该地址中的值,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源

特点

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
  • 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。结构图如下:

实现 实现对象拷贝的类,需要实现 Cloneable 接口,并重写 clone() 方法。

public class Subject {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}
public class Student implements Cloneable {

    private String name;

    private Integer age;

    private Subject subject;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", subject=" + subject +
                '}';
    }

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

测试:

@org.junit.jupiter.api.Test
public void run() throws CloneNotSupportedException {
	Subject subject = new Subject("张三");
    Student student = new Student();
    student.setName("李四");
    student.setAge(20);
    student.setSubject(subject);
    Student student2 = (Student) student.clone();
    student2.setName("王五");
    student2.setAge(22);
    Subject subject2 = student2.getSubject();
    subject2.setName("马六");
    System.out.println(student);
    System.out.println(student2);
}

输出结果: 说明: 由输出结果可以看出,通过student.clone()拷贝得到的对象student2,和student不是同一个对象。 studentstudent2 的基础数据类型的修改互不影响,而引用类型 subject 修改后是会有影响的。

浅拷贝和对象拷贝的区别

@org.junit.jupiter.api.Test
public void run2() {
    Subject subject = new Subject("张三");
    Student student = new Student();
    student.setSubject(subject);
    student.setName("李四");
    student.setAge(20);
    Student student2 = student;
    student2.setName("王五");
    student2.setAge(18);
    Subject subjectB = student2.getSubject();
    subjectB.setName("lishi");
    System.out.println(student == student2);
    System.out.println("studentA:" + student.toString());
    System.out.println("studentB:" + student2.toString());
}

输出结果: 通过结果可以看出:对象拷贝并没有生成新的对象,两个对象的地址是一样的

深拷贝

通过上面的例子可以看到,浅拷贝会带来数据安全方面的隐患,例如我们只是想修改了 student2 的 subject,但是 student 的 subject 也被修改了,因为它们都是指向的同一个地址。所以,此种情况下,我们需要用到深拷贝:

深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

特点

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
  • 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
  • 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
  • 深拷贝相比于浅拷贝速度较慢并且花销较大。

结构图如下

实现

对于 Student 的引用类型的成员变量 Subject ,需要实现 Cloneable 并重写 clone() 方法。

public class Subject implements Cloneable {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //Subject 如果也有引用类型的成员属性,也应该和 Student 类一样实现
        return super.clone();
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

Studentclone() 方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。

public class Student implements Cloneable {

    private String name;

    private Integer age;

    private Subject subject;

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", subject=" + subject +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.subject = (Subject) subject.clone();
        return student;
    }
}

测试:跟浅拷贝同样的测试方法

@org.junit.jupiter.api.Test
public void run() throws CloneNotSupportedException {
	Subject subject = new Subject("张三");
    Student student = new Student();
    student.setName("李四");
    student.setAge(20);
    student.setSubject(subject);
    Student student2 = (Student) student.clone();
    student2.setName("王五");
    student2.setAge(22);
    Subject subject2 = student2.getSubject();
    subject2.setName("马六");
    System.out.println(student);
    System.out.println(student2);
}

输出结果: 由输出结果可见,深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。

对象序列化实现深拷贝

虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。
将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

public class Age implements Serializable {
    private int age;

    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Age{" +
                "age=" + age +
                '}';
    }
}
public class Person implements Serializable {

    private String name;

    private Age age;

    public Person(String name, Age age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public Age getAge() {
        return age;
    }

    public void setAge(Age age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试:

@org.junit.jupiter.api.Test
public void run3() throws IOException, ClassNotFoundException {
    Age a = new Age(20);
    Person person = new Person("张三", a);
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(person);
    oos.flush();
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    Person person2 = (Person) ois.readObject();
    System.out.println(person == person2);
    System.out.println(person);
    System.out.println(person2);
    System.out.println("修改person的属性");
    person.setName("李四");
    a.setAge(22);
    System.out.println(person);
    System.out.println(person2);
}

输出结果: 由输出结果可以看出,通过对象序列化再反序列化来实现拷贝的对象是创建了一个新的对象