您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

简单聊聊对象浅拷贝和深拷贝,真不简单!

时间:2023-05-17 15:08:57  来源:  作者:Java极客技术


一、摘要

上篇文章中,我们有介绍到对象属性复制相关的工具,这些工具所进行的对象拷贝,其实都是浅拷贝模式。

可能有的同学会发出疑问,什么叫浅拷贝?

我们都知道,JAVA 中的数据类型分为值类型(基本数据类型)和引用类型,值类型包括 byte、short、 int、long、float、double、boolean、char 等简单数据类型,引用类型包括类、接口、数组等复杂类型。

根据数据类型的不同,在进行属性值拷贝的时候,如果是值类型,复制的是属性值,如果是复杂类型,比如对象,复制的内容可能是属性对应的内存引用地址。

因此,在 Java 中对于复杂类型的数据,也分为**浅拷贝(浅克隆)与深拷贝(深克隆)**方式,区别如下:

  • 浅拷贝:将原对象或原数组的引用直接赋给新对象或者新数组,新对象只是原对象的一个引用,也就是说不管新对象还是原对象,都是引用同一个对象
  • 深拷贝:创建一个新的对象或者数组,将原对象的各项属性的值拷贝过来,是“值”而不是“引用”,两者对象是不一样的

对于概念的解释,可能也很难理解,下面我们简单的通过几个案例向大家介绍!

二、案例实践

2.1、浅拷贝

首先我们新建两个对象,其中User关联Account对象,内容如下:

public class User {

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 账户信息
     */
    private Account account;

    //...get、set

    @Override
    public String toString() {
        return "User{" +
                "userId=" + userId +
                ", account=" + account +
                '}';
    }
}
 
public class Account {

    /**
     * 账号余额
     */
    private BigDecimal money;

    //...get、set

    @Override
    public String toString() {
        return "Account{" +
                "money=" + money +
                '}';
    }
}

使用Spring BeanUtils工具进行对象属性复制,操作如下:

// 定义某用户,账户余额 100块
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);

// 进行对象属性拷贝
User targetUser = new User();
BeanUtils.copyProperties(sourceUser, targetUser);
System.out.println("修改嵌套对象属性值前的结果:" + targetUser.toString());

//修改原始对象账户余额为200
sourceAccount.setMoney(BigDecimal.valueOf(200));

System.out.println("修改嵌套对象属性值后的结果:" + targetUser.toString());

输出结果如下:

修改嵌套对象属性值前的结果:User{userId=1, account=Account{money=100}}
修改嵌套对象属性值后的结果:User{userId=1, account=Account{money=200}}

从结果上可以很明显的得出结论:当修改原始的嵌套对象Account的属性值时,目标对象的Account对象对应的值也跟着发生变化。

很显然,这与我们预期想要的对象属性拷贝是想违背的,我们所期待的结果是:原始对象值即使发生变化,目标对象的值也不应该发生变化!

面对这种情况,怎么处理呢?

我们可以把对象Account单独拉出来,进行一次属性值拷贝,然后再进行封装,比如操作如下:

// 定义某用户,账户余额 100块
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);


// 拷贝 Account 对象
Account targetAccount = new Account();
BeanUtils.copyProperties(sourceAccount, targetAccount);

// 拷贝 User 对象
User targetUser = new User();
BeanUtils.copyProperties(sourceUser, targetUser);
targetUser.setAccount(targetAccount);
System.out.println("修改嵌套对象属性值前的结果:" + targetUser.toString());

//修改原始对象账户余额为200
sourceAccount.setMoney(BigDecimal.valueOf(200));

System.out.println("修改嵌套对象属性值后的结果:" + targetUser.toString());

输出结果如下:

修改嵌套对象属性值前的结果:User{userId=1, account=Account{money=100}}
修改嵌套对象属性值后的结果:User{userId=1, account=Account{money=100}}

即使Account对象数据发生变化,也不会改目标对象的数据,与预期结果一致!

现在的情况是User只有一个嵌套对象Account,假如像这样的对象有十几个呢,采用以上方式显然不可取。

这个时候深拷贝,该登场了!

2.2、深拷贝

Java 的深拷贝有两种实现方式,第一种是通过将对象序列化到临时文件,然后再通过反序列化方式,从临时文件中读取数据,操作案例如下!

首先所有的类,必须实现Serializable接口,推荐显式定义序列化 ID。

public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 账户信息
     */
    private Account account;

    //...get、set

    @Override
    public String toString() {
        return "User{" +
                "userId=" + userId +
                ", account=" + account +
                '}';
    }
}
 
public class Account implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 账号余额
     */
    private BigDecimal money;

    //...get、set

    @Override
    public String toString() {
        return "Account{" +
                "money=" + money +
                '}';
    }
}

// 定义某用户,账户余额 100块
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);


//把对象写入文件中
try {
    FileOutputStream fos = new FileOutputStream("temp.out");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(sourceUser);
    oos.flush();
    oos.close();
} catch (IOException e) {
    e.printStackTrace();
}

//从文件中读取对象
User targetUser = null;
try {
    FileInputStream fis = new FileInputStream("temp.out");
    ObjectInputStream ois = new ObjectInputStream(fis);
    targetUser = (User) ois.readObject();
    fis.close();
    ois.close();
}  catch (Exception e) {
    e.printStackTrace();
}

System.out.println("修改嵌套对象属性值前的结果:" + targetUser.toString());

//修改原始对象账户余额为200
sourceAccount.setMoney(BigDecimal.valueOf(200));

System.out.println("修改嵌套对象属性值后的结果:" + targetUser.toString());

输出结果:

修改嵌套对象属性值前的结果:User{userId=1, account=Account{money=100}}
修改嵌套对象属性值后的结果:User{userId=1, account=Account{money=100}}

通过序列化和反序列化的方式,可以实现多层复杂的对象数据拷贝。

因为涉及到需要将数据写入临时磁盘,性能可能会有所下降!

2.3、json 序列化和反序列化

对于对象深度拷贝,还有第二种方式,那就是采用 json 序列化和反序列化相关的技术来实现,同时性能也比将数据写入临时磁盘的方式要好很多,并且不需要显式实现序列化接口。

json 序列化和反序列化的底层思想是,将对象序列化成字符串;然后再将字符串通过反序列化方式成对象。

以jackson工具库为例,具体使用方式如下!

首先导入相关的jackson依赖包!

<!--jackson依赖-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.8</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.8</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>

其次,编写统一Json处理工具类!

public class JsonUtil {

    private static final Logger log = LoggerFactory.getLogger(JsonUtil.class);

    private static ObjectMApper objectMapper = new ObjectMapper();

    static {
        // 序列化时,将对象的所有字段全部列入
        objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
        // 允许没有引号的字段名
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        // 自动给字段名加上引号
        objectMapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
        // 时间默认以时间戳格式写,默认时间戳
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
        // 忽略空bean转json的错误
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 设置时间转换所使用的默认时区
        objectMapper.setTimeZone(TimeZone.getDefault());


        // 反序列化时,忽略在json字符串中存在, 但在java对象中不存在对应属性的情况, 防止错误
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);

        //序列化/反序列化,自定义设置
        SimpleModule module = new SimpleModule();
        // 序列化成json时,将所有的long变成string
        module.addSerializer(Long.class, ToStringSerializer.instance);
        module.addSerializer(Long.TYPE, ToStringSerializer.instance);
        // 自定义参数配置注册
        objectMapper.registerModule(module);
    }

    /**
     * 对象序列化成字符串
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> String objToStr(T obj) {
        if (null == obj) {
            return null;
        }

        try {
            return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("objToStr error: ", e);
            return null;
        }
    }

    /**
     * 字符串反序列化成对象
     * @param str
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T strToObj(String str, Class<T> clazz) {
        try {
            return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
        } catch (Exception e) {
            log.warn("strToObj error: ", e);
            return null;
        }
    }

    /**
     * 字符串反序列化成对象(数组)
     * @param str
     * @param typeReference
     * @param <T>
     * @return
     */
    public static <T> T strToObj(String str, TypeReference<T> typeReference) {
        try {
            return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
        } catch (Exception e) {
            log.warn("strToObj error", e);
            return null;
        }
    }
}

最后,在相关的位置引入即可。

// 定义某用户,账户余额 100块
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(100));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);

// json序列化、反序列化
User targetUser = JsonUtil.strToObj(JsonUtil.objToStr(sourceUser), User.class);
System.out.println("修改嵌套对象属性值前的结果:" + targetUser.toString());

//修改原始对象账户余额为200
sourceAccount.setMoney(BigDecimal.valueOf(200));

System.out.println("修改嵌套对象属性值后的结果:" + targetUser.toString());

输出结果:

修改嵌套对象属性值前的结果:User{userId=1, account=Account{money=100}}
修改嵌套对象属性值后的结果:User{userId=1, account=Account{money=100}}

与预期一致!

三、小结

本文主要围绕对象的浅拷贝和深拷贝,从使用方面做了一次简单的内容总结。

浅拷贝下,原对象和目标对象,引用都是同一个对象,当被引用的对象数据发生变化时,相关的引用者也会跟着一起变。

深拷贝下,原对象和目标对象数据是两个完全独立的存在,相互直接不受影响。

至于当前对象数据,是应该走浅拷贝还是深拷贝模式好,完全取决于当前业务的需求,没有绝对的好或者不好!

如果当前对象需要深拷贝,推荐采用 json 序列化和反序列化的方式实现,相比通过文件写入的方式进行序列化和反序列化,操作简单且性能高!



Tags:浅拷贝   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
一文搞懂Python深拷贝与浅拷贝使用和区别
1. 什么是拷贝在Python中,拷贝是指创建一个新的对象,其中包含了原始对象的值,以便于在不改变原始对象的情况下进行操作。拷贝在处理数据时非常有用,特别是当我们需要对数据进行...【详细内容】
2023-09-07  Search: 浅拷贝  点击:(154)  评论:(0)  加入收藏
浅谈Python中的深、浅拷贝
引言作为一名Python开发者,在进行数据处理和传递时,我们经常会遇到对象的复制问题。Python提供了深拷贝和浅拷贝两种方式来处理对象的复制需求。这两种拷贝方式有着不同的特点...【详细内容】
2023-08-23  Search: 浅拷贝  点击:(266)  评论:(0)  加入收藏
谈谈JS中的浅拷贝与深拷贝
在前端面试当中,经常会被问到浅拷贝与深拷贝的问题,这主要是考察面试者对基本数据类型和引用数据类型的理解,今天我们就通过本篇帮助大家详细理解浅拷贝和深拷贝的概念以及实现...【详细内容】
2023-06-27  Search: 浅拷贝  点击:(185)  评论:(0)  加入收藏
深入理解Python数据结构中的深浅拷贝
前言今天 给大家解析Python常见面试题:Python数据中的深浅拷贝。在Python中,有时我们需要复制一个对象,以便在不改变原始对象的情况下进行操作。Python提供了两种复制对象的方...【详细内容】
2023-05-25  Search: 浅拷贝  点击:(346)  评论:(0)  加入收藏
简单聊聊对象浅拷贝和深拷贝,真不简单!
一、摘要上篇文章中,我们有介绍到对象属性复制相关的工具,这些工具所进行的对象拷贝,其实都是浅拷贝模式。可能有的同学会发出疑问,什么叫浅拷贝?我们都知道,Java 中的数据类型分...【详细内容】
2023-05-17  Search: 浅拷贝  点击:(328)  评论:(0)  加入收藏
深入剖析JavaScript中深浅拷贝
最近有一位00后的小妹妹粉丝私信小编说自己很喜欢编程,目前在某公司实习前端开发工作,说到现在为止还没有搞懂JavaScript中深拷贝和浅拷贝这个问题,同时也在网上看了很多关于深...【详细内容】
2023-05-12  Search: 浅拷贝  点击:(534)  评论:(0)  加入收藏
深拷贝和浅拷贝:如何选择最适合你的对象复制技术?
Java中的对象复制主要有三种方式:clone、深拷贝和浅拷贝。这些技术对于Java开发人员来说非常重要,因为它们可以帮助开发人员管理复杂的数据结构。本文将详细讨论这三种技术,...【详细内容】
2023-05-06  Search: 浅拷贝  点击:(480)  评论:(0)  加入收藏
原来深拷贝与浅拷贝是这样?
为了让读者更好的理解深浅拷贝,在讲深浅拷贝之前要引入基本数据类型 , 引用数据类型 和 数据储存(栈和堆)这几个概念,如果已经理解,可直接跳过这一part。JS数据类型Q:前端面试常...【详细内容】
2019-09-03  Search: 浅拷贝  点击:(670)  评论:(0)  加入收藏
5张图彻底理解Python中的浅拷贝与深拷贝
假设你去面试 Python 开发岗,面试官如果对基础比较看重的话,那么很可能会问你这样的问题“谈谈你对 Python 中的浅拷贝和深拷贝的理解?”若平时你在开发中像我一样,过度使用 dee...【详细内容】
2019-09-03  Search: 浅拷贝  点击:(760)  评论:(0)  加入收藏
浅谈Python深浅拷贝
对于各种编程语言,深浅拷贝总可以让初学者头疼不已。而Python--这门十分受欢迎的语言也不例外。下面,分享下自己对Python深浅拷贝的理解。 浅复制后,两个变量同时变化 深复制后...【详细内容】
2019-08-15  Search: 浅拷贝  点击:(957)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条