Java 中的浅拷贝和深拷贝

无论是浅拷贝还是深拷贝,都可以通过 Object 类的 clone() 方法来完成:

/**
 * 拷贝
 *
 * @author qiaohaojie
 * @date 2023/3/5  15:58
 */
public class CloneTest {
    public static void main(String[] args) throws Exception {
        Person person1 = new Person(23, "青花椒");
        Person person2 = (Person) person1.clone();

        System.out.println("浅拷贝后:");
        System.out.println("person1:" + person1); // person1:Person{age=23, name='青花椒'}
        System.out.println("person2:" + person2); // person2:Person{age=23, name='青花椒'}
    }
}

扒一下 clone() 方法的源码:

// Java 1.8
protected native Object clone() throws CloneNotSupportedException;

// Java 9
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;

其中,@HotSpotIntrinsicCandidate注解是 Java 9 引入的一个注解,被它标记的方法,在 HotSpot 虚拟机中会有一套高效的实现。需要注意的是,clone() 方法同时是一个本地(native)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。

1. 浅拷贝

1.1 基本数据类型

Person 类有两个字段,分别是 age 和 name,然后重写 toString() 方法:

/**
 * person类
 *
 * @author qiaohaojie
 * @date 2023/3/5  16:09
 */
@Data
public class Person implements Cloneable {

    private int age;
    private String name;

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


	@Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

Cloneable 接口是一个标记接口,接口里没有内容:

public interface Cloneable {
}

它只是一个标记接口,为什么要实现它呢?

标记接口的作用其实就是用来表示某个功能在执行的时候是合法的。如果一个类没有实现 Cloneable 接口,即使它重写了 clone() 方法了,但它依然是无法调用该方法进行对象克隆的,程序在执行 clone() 方法的时候会抛出 CloneNotSupportedException 异常:

Exception in thread "main" java.lang.CloneNotSupportedException

测试类:

/**
 * 浅拷贝测试-基本类型字段
 *
 * @author qiaohaojie
 * @date 2023/3/5  15:58
 */
public class CloneTest {
    public static void main(String[] args) throws Exception {
        Person person1 = new Person(23, "青花椒");
        Person person2 = (Person) person1.clone();

        System.out.println("浅拷贝后:");
        System.out.println("person1:" + person1); // person1:Person@86accb4e{age=23, name='青花椒'}
        System.out.println("person2:" + person2); // person2:Person@6c58a0a6{age=23, name='青花椒'}

        person2.setName("程序猿");
        person1.swtAge(66);

        System.out.println("修改后:");
        System.out.println("person1:" + person1); // person1:Person@86accb4e{age=23, name='青花椒'}
        System.out.println("person2:" + person2); // person2:Person@6c58a0a6{age=23, name='程序猿'}
    }
}

步骤大概是这样的:

  1. 通过 new 关键字声明了一个 Person 对象,并将其赋值给 person1;
  2. 调用 clone() 方法进行对象的拷贝,并将其赋值给 person2;
  3. 打印 person1 和 person2;
  4. 将 person2 的 name 字段修改为 “程序猿”;
  5. 打印 person1 和 person2。

从结果可以看出,浅拷贝后 person1 和 person2 引用了不同的对象,但是值是相同的,说明拷贝成功了。之后再修改 person2 的 name 字段时,引用是这样的:
Java 中的浅拷贝和深拷贝_第1张图片

1.2 引用数据类型

再自定义一个 Student 类,有 score 和 name 两个字段:

/**
 * 学生类
 *
 * @author qiaohaojie
 * @date 2023/3/5  16:31
 */
@Data
public class Student implements Cloneable {

    private double score;
    private String name;

    public Student(double score, String name) {
        this.score = score;
        this.name = name;
    }

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

    @Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "score=" + score +
                ", name='" + name + '\'' +
                '}';
    }
}

修改 Person 类,把 Student 类对象当做一个成员变量:

/**
 * person类
 *
 * @author qiaohaojie
 * @date 2023/3/5  16:09
 */
@Data
public class Person implements Cloneable {

    private int age;
    private String name;
    private Student student;

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

    // 浅拷贝
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", student=" + student +
                '}';
    }
}

测试类:

/**
 * 浅拷贝测试-引用类型字段
 *
 * @author qiaohaojie
 * @date 2023/3/5  16:46
 */
public class CloneTest2 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(23, "青花椒");
        Student student1 = new Student(93.2, "张三");
        person1.setStudent(student1);

        Person person2 = (Person) person1.clone();
        System.out.println("浅拷贝后:");
        System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
        System.out.println("person2:" + person2); // person2:Person@9853e6e6{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}

        Student student2 = person2.getStudent();
        student2.setName("李四");
        student2.setScore(88.8);
        System.out.println("person2.变更后:");

        System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=88.8, name='李四'}}
        System.out.println("person2:" + person2); // person2:Person@1f00b209{age=23, name='青花椒', student=Student@67e260bd{score=88.8, name='李四'}}
    }
}

步骤大概是这样的:

  1. 通过 new 关键字声明了一个 Person 对象,并将其赋值给 person1;
  2. 通过 new 关键字声明了一个 Stident 对象,并将其赋值给 student1;
  3. 把 person1 的 student 字段设置为 student1;
  4. 调用 clone() 方法进行对象的拷贝,并将其赋值给 person2;
  5. 打印 person1 和 person2;
  6. 获取 person2 的 student 字段,并将其赋值给 student2;
  7. 把 student2 的 name 字段修改为 “李四”,score 字段修改为 88.8;
  8. 打印 person1 和 person2。

与基本数据类型拷贝不同的是,person2.student 修改后,person1.student 也发生了改变。这是因为字符串 String 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变:
Java 中的浅拷贝和深拷贝_第2张图片
值得注意的是,浅拷贝克隆的对象中,引用类型的字段指向的是同一个对象,当改变任何一个对象,另外一个对象也会随之改变,除去字符串的特殊性外。

2. 深拷贝

深拷贝和浅拷贝不同的是,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象时,另外一个对象不会随之改变。

Student 类,重写了 clone() 方法,并实现了 Cloneable 接口,为的就是深拷贝时也能够克隆该字段:

/**
 * 学生类
 *
 * @author qiaohaojie
 * @date 2023/3/5  16:31
 */
@Data
public class Student implements Cloneable {

    private double score;
    private String name;

    public Student(double score, String name) {
        this.score = score;
        this.name = name;
    }

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

    @Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "score=" + score +
                ", name='" + name + '\'' +
                '}';
    }
}

Person 类,与之前不同的是,clone() 方法中,不再只调用 Object 的 clone() 方法对 Person 进行克隆了,还对 Student 也进行了克隆:

/**
 * person类
 *
 * @author qiaohaojie
 * @date 2023/3/5  16:09
 */
@Data
public class Person implements Cloneable {

    private int age;
    private String name;
    private Student student;

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


    // 深拷贝
    @Override
    public Object clone() throws CloneNotSupportedException {
        Person person = (Person) super.clone();
        person.setStudent(student);
        person.setStudent((Student) person.getStudent().clone());
        return super.clone();
    }


    @Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", student=" + student +
                '}';
    }
}

测试类:

/**
 * 深拷贝测试
 *
 * @author qiaohaojie
 * @date 2023/3/5  17:06
 */
public class CloneTest3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(23, "青花椒");
        Student student1 = new Student(93.2, "张三");
        person1.setStudent(student1);

        Person person2 = (Person) person1.clone();
        System.out.println("深拷贝后:");
        System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
        System.out.println("person2:" + person2); // person2:Person@1f00b209{age=23, name='青花椒', student=Student@9853e6e6{score=93.2, name='张三'}}

        Student student2 = person2.getStudent();
        student2.setName("李四");
        student2.setScore(88.8);
        System.out.println("person2.变更后:");

        System.out.println("person1:" + person1); // person1:Person@ee8f2be0{age=23, name='青花椒', student=Student@67e260bd{score=93.2, name='张三'}}
        System.out.println("person2:" + person2); // person2:Person@1f00b209{age=23, name='青花椒', student=Student@9853e6e6{score=88.8, name='李四'}}
    }
}

在深拷贝中,不只是 person1 和 person2 是不同的对象,它们中的 student1 和 student2 也是不同的对象。所以,改变 person2 中的 student2 并不会影响到 person1:
Java 中的浅拷贝和深拷贝_第3张图片

但是,通过 clone() 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 clone() 方法,当嵌入的对象比较多的时候,就很容易出错了。

3. 序列化的方式深拷贝

利用序列化的方式进行深拷贝,序列化时将对象写到流中便于传输,而反序列化时将对象从流中读取出来。

写入流中的对象就是对原始对象的拷贝。但是需要注意,每个要序列化的类都要实现 Serializable 接口,这个接口与 Cloneable 接口类似,都是标记型接口。

继续以 Person 类和 Student 举例:

Student 类:

/**
 * student类
 *
 * @author qiaohaojie
 * @date 2023/3/9  22:38
 */
@Data
public class Studentl implements Serializable {

    private double score;
    private String name;

    public Studentl(double score, String name) {
        this.score = score;
        this.name = name;
    }

    @Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "score=" + score +
                ", name='" + name + '\'' +
                '}';
    }
}

Person 类:

/**
 * person类
 *
 * @author qiaohaojie
 * @date 2023/3/9  22:38
 */
@Data
public class Personl implements Serializable {

    private int age;
    private String name;
    private Studentl student;

    public Personl(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return super.toString().substring(18) + "{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", student=" + student +
                '}';
    }

    //深度拷贝
    public Object deepClone() throws IOException, ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);

        oos.writeObject(this);

        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);

        return ois.readObject();
    }
}

Person 类也需要实现 Serializable 接口,并且在该类中,增加了一个 deepClone() 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。

测试类:

/**
 * 序列化
 *
 * @author qiaohaojie
 * @date 2023/3/9  22:41
 */
public class CloneTest4 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Personl person1 = new Personl(23, "青花椒");
        Studentl student1 = new Studentl(93.2, "张三");
        person1.setStudent(student1);

        Personl person2 = (Personl) person1.deepClone();
        System.out.println("深拷贝后:");
        System.out.println("person1:" + person1); // person1:Personl@ee8f2be0{age=23, name='青花椒', student=Studentl@67e260bd{score=93.2, name='张三'}}
        System.out.println("person2:" + person2); // person2:Personl@1f00b209{age=23, name='青花椒', student=Studentl@9853e6e6{score=93.2, name='张三'}}

        Studentl student2 = person2.getStudent();
        student2.setName("李四");
        student2.setScore(88.8);
        System.out.println("person2.变更后:");

        System.out.println("person1:" + person1); // person1:Personl@ee8f2be0{age=23, name='青花椒', student=Studentl@67e260bd{score=93.2, name='张三'}}
        System.out.println("person2:" + person2); // person2:Personl@1f00b209{age=23, name='青花椒', student=Studentl@9853e6e6{score=88.8, name='李四'}}
    }
}

这种方式也可以实现深拷贝,但是由于是序列化涉及到输入流和输出流的读写,在性能上要比 HotSpot 虚拟机实现的 clone() 方法差很多。

你可能感兴趣的:(Java,java,深拷贝,浅拷贝)