写点什么

Java 中的深拷贝和浅拷贝

作者:Ayue、
  • 2021 年 12 月 30 日
  • 本文字数:2205 字

    阅读完需:约 7 分钟

概述

对象之间的转换在开发中是最常见的,实体对象和数据显示层(VO,DTO 等)的转换,通常有 2 种做法:


  1. set 方法赋值,这种方式一般使得代码冗余,而且极不美观;

  2. 对象拷贝工具类,如 Spring 的 BeanUtils.copyProperties 。


而我最近在使用 Spring 的拷贝工具类的时候碰到了属性值丢失的情况,先看这样一个栗子。


订单实体类到 VO 层的转换:



转换后的结果如下:



从上面可以看到 2 个问题:


  1. 对象 copy 后没有 id 并没有赋值;

  2. VO 对象的改变影响了原订单的值。

问题一

先看第 1 个问题。


一般在项目开发中会把实体类中一些公共的字段(如 id,创建日期等)抽为一个公共的实体,如下:


BaseEntity



可以看到这里的 BaseEntity 中定义的 id 是一个泛型,因为在项目中有 2 中主键类型,一种是 String 类型的,一种是 Long 类型的,如下:


LongBaseEntity,主键为 Long 类型的,实体类主键若为 Long,继承它即可。



UUIDBaseEntity,主键为 String 类型的,实体类主键若为 String ,继承它即可。



而订单类如下,一般来说这里不会存在其他对象,这里订单详情的目的仅仅是为了演示上面的第二个问题



订单详情:



数据展示 VO:



然后当我把 TicketOrder 转为 TicketOrderVO,发现 TicketOrder 的 id 并没有 copy 过来,致使到后续的逻辑无法走通,因为后续需要拿 TicketOrderVO 的 id。


于是当我进入 Spring 的 BeanUtils.copyProperties 源码发现了这样一段代码:



对于上面的注释大致可以翻译为:如果属性是泛型,则不可解析


在看看我们定义的主键 id 确实为泛型,所以导致了这里的 TicketOrderVO 中的 id 不会被赋值。


怎么处理呢?


最简单的方式就是单独给 id set 值,或者采用其他的 Bean Copy 工具,我之前也介绍过一个工具,可以参考这篇文章:Orika对象转换,相比于 Spring 的反射效率更高。

问题二

再看第 2 个问题,这里涉及到浅拷贝和深拷贝的问题,什么是浅拷贝和深拷贝?

浅拷贝和深拷贝

浅拷贝:对基本数据类型(int,long...)进行值传递,对引用数据类型(Object)进行引用传递般的拷贝。


深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,既然是新对象,复制后修改当然不会影响原对象的属性内容。


而上面使用的 BeanUtils.copyProperties 就是浅拷贝,所以当 TicketOrderVO 中的 detail 属性发生改变的时候,TicketOrder 中的 detail 属性也发生了改变,即他们在栈中的指向是同一个引用,所以当 TicketOrderVO 中的 detail 发生改变,原 TicketOrder 也会发生改变。


可能看到这会有点懵逼,是不是忘了基本数据类型和引用类型,值传递和引用传递?

基本数据类型和引用类型

Java 中一共有四类八种基本数据类型,如下表:



记住:String 不是基本数据类型


除了这四类八种基本类型,其它的都是对象,也就是引用类型,包括数组。

值传递和引用传递

对于值传递和引用传递,如下:


值传递:是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。


引用传递:是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。


那就有人有疑问了,既然 String 是引用类型,为什么它的值不发生改变?


因为 String,Long 等都是 final 修饰的类呀,当然不会被修改。


可以参考:值传递和引用传递


也可以去看看我的 JVM 系列,了解更多堆栈的知识:JVM相关

如何实现深拷贝

clone()

在 Object 类中定义了一个 clone 方法,但这个方法在不重写的情况下,其实也是浅拷贝的。


如果想要实现深拷贝,就需要重写 clone 方法,而想要重写 clone 方法,就必须实现 Cloneable,否则会报 CloneNotSupportedException 异常。


修改代码:


TicketOrderDetail



TicketOrder



测试



结果



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

序列化

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


注:需要序列化的类都要实现 Serializable 接口,如果某个字段不需要序列化,可以将其声明为 transient


序列化的方式有很多,IO 流,JSON 工具,把对象序列化成 JSON 字符串,然后再从字符串中反序列化成对象。


IO 流实现


//深度拷贝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();}
复制代码


fastjson 实现


TicketOrder newOrder = JSON.parseObject(JSON.toJSONString(order), TicketOrder.class);
复制代码


除此之外,还可以使用 Apache Commons Lang 中提供的 SerializationUtils 工具实现。


TicketOrder newOrder = (TicketOrder) SerializationUtils.clone(order);
复制代码


一个 BeanUtils.copyProperties的使用不仅引出了存在的问题,同时还涉及到很多的基础知识。

参考

深拷贝和浅拷贝


Java 的深拷贝和浅拷贝

用户头像

Ayue、

关注

个人站点:javatv.net 2019.10.16 加入

学习知识,目光坚毅

评论

发布
暂无评论
Java中的深拷贝和浅拷贝