Java 中的浅拷贝和深拷贝

news/2024/11/8 12:10:05/

无论是浅拷贝还是深拷贝,都可以通过 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;}@Overridepublic 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 字段时,引用是这样的:
在这里插入图片描述

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;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic 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;}// 浅拷贝@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic 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 是不可变对象,一个新的值必须在字符串常量池中开辟一段新的内存空间,而自定义对象的内存地址并没有发生改变,只是对应的字段值发生了改变:
在这里插入图片描述
值得注意的是,浅拷贝克隆的对象中,引用类型的字段指向的是同一个对象,当改变任何一个对象,另外一个对象也会随之改变,除去字符串的特殊性外。

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;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic 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;}// 深拷贝@Overridepublic Object clone() throws CloneNotSupportedException {Person person = (Person) super.clone();person.setStudent(student);person.setStudent((Student) person.getStudent().clone());return super.clone();}@Overridepublic 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:
在这里插入图片描述

但是,通过 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;}@Overridepublic 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;}@Overridepublic 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() 方法差很多。


http://www.ppmy.cn/news/31614.html

相关文章

Angular学习之ControlValueAccessor接口详解

ControlValueAccessor 是什么?为什么需要使用 ?下面本篇文章就来带大家了解Angular中的ControlValueAccessor组件接口,希望对大家有所帮助! ControlValueAccessor 是什么? 简单来说ControlValueAccessor是一个接口&am…

STA环境

目录1. CMOS逻辑门2. 波形3. 时钟3.1. 指定时钟create_clock时钟延迟set_clock_latency 时钟不确定度set_clock_uncertainty 跨时钟域set_false_path3.2. 衍生时钟3.3. 虚拟时钟4. 时序路径2.1. 输入路径2.2. 输出路径2.3. 点对点约束本文介绍在执行静态时序分析(St…

AI人工智能 - 初探

1.应用场景 主要用于了解和系统学习AI,从而可以在工作生活中利用AI做一些事。 2.学习/操作 1.文档阅读 下面的内容来自于与chatGPT的对话 2.整理输出 介绍AI 人工智能(Artificial Intelligence,简称AI)是计算机科学中的一个分支&…

自定义javax.validation校验枚举类

枚举类单一情况 package com.archermind.cloud.phone.dto.portal.external.validation.validator;import com.archermind.cloud.phone.dto.portal.external.validation.constraints.EnumValidation; import lombok.extern.slf4j.Slf4j;import javax.validation.ConstraintVali…

DML 添加、修改、删除数据

目录 DML 一、添加数据 1、给指定字段添加数据 2、给全部字段添加数据 3、批量添加数据 二、修改数据 三、删除数据 DML DML英文全称是Data Manipulation Language(数据操作语言),用来对数据库中表的数据记录进行增、删、改操作。 一、添加数据 1、给指定字…

K8s kubectl 高效使用技巧,搞定批处理!

1.kubectl用法详解 1. kubectl语法 kubectl [command] [Type] [NAME] [flags] command: 子命令,用于操作kubernetes集群资源对象的命令,例如:create, delete, describe, get, apply等等 TYPE: 资源对象的类型,区分大小写&#…

ASEMI高压MOS管10N65参数,10N65规格,10N65封装

编辑-Z ASEMI高压MOS管10N65参数: 型号:10N65 漏极-源极电压(VDS):650V 栅源电压(VGS):30V 漏极电流(ID):10A 功耗(PD&#xff…

爬取知乎问题答案

参考博客:基于Python知乎回答爬虫 jieba关键字统计可视化_知乎爬虫搜索关键词_菠萝柚王子的博客-CSDN博客 1、安装依赖包 import numpy import requests import certifi from PIL import Image from lxml import etree import jieba from wordcloud import WordClo…