Java中的深拷贝和浅拷贝介绍

news2024/9/29 17:32:59

文章目录

    • 基本类型和引用类型
    • Clone方法
    • 浅拷贝
    • 深拷贝
    • 小结


在讲解什么是深拷贝和浅拷贝之前,我们先来了解一下什么是基本类型和引用类型。

基本类型和引用类型

基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。

引用类型则包括类、接口、数组、枚举等。

Java 将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,通过栈中的引用指向堆中存放的数据。

在这里插入图片描述

上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在 栈中,然后指向堆的内存空间。

下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。

Clone方法

本篇博客我们讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:

protected native Object clone() throws CloneNotSupportedException; 

这是一个用 native 关键字修饰的方法,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。

浅拷贝

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

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

public class Address {

    private String province;
    private String city;

    public void setAddress(String province, String city) {
        this.province = province;
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address [province=" + province + ", city=" + city + "]";
    }
}
public class Student implements Cloneable {

    private String name;
    private int age;
    private Address address;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
        this.address = new Address();
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public void setAddress(String province, String city) {
        address.setAddress(province, city);
    }

    public void display(String name) {
        System.out.println(name + ":" + "name=" + name + ", age=" + age + "," + address);
    }

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

这是一个我们要进行赋值的原始类 Student。下面我们产生一个 Student对象,并调用其 clone 方法复制一个新的对象。

注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。

测试:

    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("小明",20);
        s1.setAddress("安徽","合肥");
        Student s2 = (Student) s1.clone();
        System.out.println("S1:"+s1);
        System.out.println("s1.getName:"+s1.getName().hashCode());
        System.out.println("S2:"+s2);
        System.out.println("s2.getName:"+s2.getName().hashCode());

        s1.display("s1");
        s2.display("s2");
        s2.setAddress("安徽","安庆");
        s1.display("s1");
        s2.display("s2");
    }

输出结果:

S1:org.example.jvm.Student@2a84aee7
s1.getName:756703
S2:org.example.jvm.Student@a09ee92
s2.getName:756703
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=合肥]
s1:name=s1, age=20,Address [province=安徽, city=安庆]
s2:name=s2, age=20,Address [province=安徽, city=安庆]

首先我们创建一个Student类的对象 s1,其name 为小明,age为20,地址类 Address 两个属性为 安徽和合肥。接着我们调用 clone() 方法复制另一个对象 s2,接着打印这两个对象的内容。

分析结果:

  • 从第 1 行和第 3 行打印结果来看,这是两个不同的对象。

  • 从第 5 行和第 6 行打印的对象内容看,原对象 s1 和克隆出来的对象 s2 内容完全相同。

  • 我们更改一下克隆对象 s2 的属性 Address 为安徽安庆(原对象 s1 是安徽合肥),但是从第 7 行和第 8 行打印结果来看,原对象 s1 和克隆对象 s2 的 Address 属性都被修改了。

  • 对象 Student 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。

在这里插入图片描述

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

深拷贝

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

在这里插入图片描述

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

public class Address implements Cloneable{

    private String province;
    private String city;

    public void setAddress(String province, String city) {
        this.province = province;
        this.city = city;
    }

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

    @Override
    public String toString() {
        return "Address [province=" + province + ", city=" + city + "]";
    }
}

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

public class Student implements Cloneable {

    private String name;
    private int age;
    private Address address;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
        this.address = new Address();
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public void setAddress(String province, String city) {
        address.setAddress(province, city);
    }

    public void display(String name) {
        System.out.println(name + ":" + "name=" + name + ", age=" + age + "," + address);
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student s = (Student) super.clone();
        s.address = (Address) address.clone();
        return s;
    }
}

测试:

    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("小明",20);
        s1.setAddress("安徽","合肥");
        Student s2 = (Student) s1.clone();
        System.out.println("S1:"+s1);
        System.out.println("s1.getName:"+s1.getName().hashCode());
        System.out.println("S2:"+s2);
        System.out.println("s2.getName:"+s2.getName().hashCode());

        s1.display("s1");
        s2.display("s2");
        s2.setAddress("安徽","安庆");
        s1.display("s1");
        s2.display("s2");
    }

输出结果:

S1:org.example.jvm.Student@2a84aee7
s1.getName:756703
S2:org.example.jvm.Student@a09ee92
s2.getName:756703
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=合肥]
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=安庆]

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

注意:

但是这种做法有个弊端,这里我们Student类只有一个Address引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。

还有一种方式可以实现深拷贝:利用序列化

序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。

这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

public class Address implements Serializable {

    private String province;
    private String city;

    public void setAddress(String province, String city) {
        this.province = province;
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address [province=" + province + ", city=" + city + "]";
    }
}
public class Student implements Serializable {

    private String name;
    private int age;
    private Address address;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
        this.address = new Address();
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public void setAddress(String province, String city) {
        address.setAddress(province, city);
    }

    public void display(String name) {
        System.out.println(name + ":" + "name=" + name + ", age=" + age + "," + address);
    }

    //深度拷贝
    public Object deepClone() throws Exception{
        // 序列化
        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();
    }

}

测试:

    public static void main(String[] args) throws Exception {
        Student s1 = new Student("小明",20);
        s1.setAddress("安徽","合肥");
        Student s2 = (Student) s1.deepClone();
        System.out.println("S1:"+s1);
        System.out.println("s1.getName:"+s1.getName().hashCode());
        System.out.println("S2:"+s2);
        System.out.println("s2.getName:"+s2.getName().hashCode());

        s1.display("s1");
        s2.display("s2");
        s2.setAddress("安徽","安庆");
        s1.display("s1");
        s2.display("s2");
    }

输出结果:

S1:org.example.jvm.Student@3f99bd52
s1.getName:756703
S2:org.example.jvm.Student@1f17ae12
s2.getName:756703
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=合肥]
s1:name=s1, age=20,Address [province=安徽, city=合肥]
s2:name=s2, age=20,Address [province=安徽, city=安庆]

因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。

小结

至此,相信什么是深拷贝,什么是浅拷贝,相信你一定明白了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/586105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue3 项目相关

vite 项目起步式 npm create vite - 1.命名项目名称- 2. 选择技术框架- 3. 进入项目文件夹 npm i 安装依赖,- 4. npm run dev 运行项目配置 package.json 文件 ,使项目运行后自动再浏览器中打开。 在 dev 运行命令后添加一个 --open 即可。 "script…

微信小程序初识

微信小程序 因(ios,android)多平台彼此间并不互通,所以开发需要两个不同平台的开发团推队,所以微信小程序因此诞生。 小程序的优点 快速加载更强大的能力原生的体验易用且安全的微信数据开放高效和简单的开发 首先 根据自己的情况安装微…

弄懂软件测试左移和右移,靠它就行

软件测试技术应当贯穿整个软件开发生命周期、对软件产品(包括阶段性产品)进行验证和确认的活动过程,其核心目标是尽快尽早地发现软件产品中所存在的各种问题 bug—— 与用户需求、预先定义的不一致性。 传统的软件测试流程是 接到项目后参与…

cubemx stm32 pca9685pw模块 16路PWM 可用于舵机驱动 驱动代码

资料 淘宝链接请点这里 淘宝资料资料: 链接:https://pan.baidu.com/s/1Kda-c7QdZdQ03FBMa0zeRA 提取码:1234 pca9685pw介绍 这个模块是 I2C 通信控制 16 路 PWM 的模块。 所有路的 频率 是统一设置的,所以每一路的频率都一样&a…

java单元测试( Hamcrest 断言)

java单元测试( Hamcrest 断言) 单元测试特征: 1 范围狭窄 2 限于单一类或方法 3 体积小 为什么要编写单元测试? 为了防止错误(很明显!) 而且还可以提高开发人员的生产力,因为单元测试: (1) 帮助实施——在…

网工内推 | 经验不限,国企招网工,IE认证优先,五险一金

01 一九零五(北京)网络科技有限公司 🔷招聘岗位:网络工程师 🔷职责描述: 1、负责公司内部现有网络配置及调优; 2、负责IT机房的网络和安全的日常维护工作; 3、负责IT机房的紧急故…

轻松掌握redis缓存穿透、击穿、雪崩问题及解决方案(20230529版)

1、缓存穿透 所谓缓存穿透就是非法传输了一个在数据库中不存在的条件,导致查询redis和数据库中都没有,并且有大量的请求进来,就会导致对数据库产生压力,解决这一问题的方法如下: 1、使用空缓存解决 对查询到值是空的…

【Python开发】FastAPI 02:请求参数—路径参数、查询参数

进行接口请求时,请求参数是重中之重了!请求参数指客户端向服务端发送请求时,需要传递给服务端的参数,包括路径参数、查询参数、请求体等。举个例子,如果客户端想要获取某个用户的信息,可以向服务端发送一个…

PHPMySQL基础(一):创建数据库并通过PHP进行连接

PHP同样可以对数据库进行连接,并且实现增删改查、登录注册等功能,这一篇写一下怎么使用PHP去连接MySQL数据库 目录 一、创建数据库 1.1 登录页面 1.2 创建数据库 1.3 创建数据库表 1.4 添加表字段 1.5 插入数据 1.6 导出和导入 二、PHP连接数据…

华为OD机试真题B卷 Java 实现【报文重排序】,附详细解题思路

一、题目描述 对报文进行重传和重排序是常用的可靠性机制&#xff0c;重传缓冲区内有一定数量的子报文&#xff0c;每个子报文在原始报文中的顺序已知&#xff0c;现在需要恢复出原始报文。 二、输入描述 输入第一行为N&#xff0c;表示子报文的个数&#xff0c;0 < N &l…

SpringBoot 配置文件和日志文件

目录 一、SpringBoot配置文件 配置文件的格式 .properties配置文件格式 .yml配置文件格式 .properties 与 .yml的区别 配置文件的读取 .properties 与 .yml的区别 设置不同环境的配置⽂件 二、SpringBoot日志文件 日志打印的步骤 得到日志对象 方法一&#xff1a;使…

vulnhub靶场之RAGNAR LOTHBROK: 1

1.信息收集 探测存活主机&#xff0c;输入&#xff1a;netdiscover -r 192.168.239.0/24 &#xff0c;发现192.168.239.178存活。 对目标主机192.168.239.178进行端口扫描&#xff0c;发现存活21(ftp)、80、443、3306端口。 浏览器访问http://192.168.239.178&#xff0c;发…

设计模式 - 代理模式

基本介绍: 代理模式&#xff1a;为一个对象提供一个替身&#xff0c;以控制对这个对象的访问。即通过代理 对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的 功能操作,即扩展目标对象的功能。被代理的对象可以是远程对象、创建开销大的对象或需要安全控…

chatgpt赋能python:Python三次方根的用途和计算方法

Python三次方根的用途和计算方法 如果您是一位Python编程工程师&#xff0c;您可能会经常需要用到Python的数学计算功能。在这篇文章中&#xff0c;我们将探讨Python三次方根的概念和使用&#xff0c;以及如何在Python中计算三次方根。 什么是三次方根&#xff1f; 三次方根…

SpringBoot 框架

SpringBoot 框架 SpringBoot 简介SpringBoot 开发步骤SpringBoot工程快速启动SpringBoot概述起步依赖自动装配切换web服务器 配置文件配置文件格式yaml格式yaml配置文件数据读取Value注解读取配置文件Environment对象自定义对象多环境配置 SpringBoot 整合 SpringBoot 简介 Sp…

这个 计数排序详解过程 我能吹一辈子!!!

文章目录 计数排序概念计数排序算法思路计数排序算法过程计数排序代码实现计数排序缺陷 计数排序概念 计数排序是一个非基于比较的排序算法&#xff0c;元素从未排序状态变为已排序状态的过程&#xff0c;是由额外空间的辅助和元素本身的值决定的。该算法于1954年由 Harold H.…

流行框架(一)EventBus(组件通信)、ARouter(页面跳转)

文章目录 EventBus基本使用EventBus三要素五种线程模式使用步骤EventBus黏性事件&#xff08;sticky event&#xff09; 工作原理中介者模式源码解读Event Bus中介者对象register() / 注册发布事件 / post Arouter组件化开发组件化开发的优点组件分层方案组件化的gradle工程 AR…

C919商业首航 背后功臣风洞实验室有多牛

5月28日&#xff0c;国产大型客机C919&#xff0c;顺利完成商业首航。 首航背后意味着该机型从研发、制造、取证到投运全面贯通&#xff0c;广大旅客终于有机会坐国产大飞机出行了。 很多人不知道C919其实是依托我国独立自主设计制造的世界级风洞群和风洞实验室反复测试“百炼…

Linux部署jumpserver堡垒机及问题汇总

部署过程相对复杂&#xff01;请耐心浏览&#xff01; 目录 一、jumpserver堡垒机简介 1.1 为什么需要使用堡垒机? 1.2 堡垒机主要功能 二、准备工作 2.1 关闭防火墙以及SElinux 1.2 时间同步 1.3 更改主机名 1.4 yum源备份及准备 1.5 安装初始工具 1.6 修改系统字…

基于PS-InSAR技术的形变监测分析流程

基于PS-InSAR技术的形变监测分析流程 文章目录 基于PS-InSAR技术的形变监测分析流程1. 背景知识1.1 PS-InSAR技术1.1.1 雷达干涉测量1.1.2 InSAR技术1.1.3 技术原理1.1.4 技术特征1.1.5 技术优化1.1.6 应用 1.2 Sentinel-1数据1.2.1 Sentinel-1简介1.2.2 Sentinel-1扫描模式1.2…