Java 中的深拷贝和浅拷贝
概述
对象之间的转换在开发中是最常见的,实体对象和数据显示层(VO,DTO 等)的转换,通常有 2 种做法:
set 方法赋值,这种方式一般使得代码冗余,而且极不美观;
对象拷贝工具类,如 Spring 的 BeanUtils.copyProperties 。
而我最近在使用 Spring 的拷贝工具类的时候碰到了属性值丢失的情况,先看这样一个栗子。
订单实体类到 VO 层的转换:
转换后的结果如下:
从上面可以看到 2 个问题:
对象 copy 后没有 id 并没有赋值;
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 流实现:
fastjson 实现:
除此之外,还可以使用 Apache Commons Lang 中提供的 SerializationUtils 工具实现。
一个 BeanUtils.copyProperties
的使用不仅引出了存在的问题,同时还涉及到很多的基础知识。
评论