举例说明深拷贝和浅拷贝的区别

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第25天,点击查看活动详情

概述

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

  1. set 方法赋值,这种方式一般使得代码冗余,而且极不美观;
  2. 对象拷贝工具类,如 Spring 的 BeanUtils.copyProperties 。

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

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

image-20211123204233625

转换后的结果如下:

image-20211123202748189

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

  1. 对象 copy 后没有 id 并没有赋值;
  2. VO 对象的改变影响了原订单的值。

问题一

先看第 1 个问题。

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

BaseEntity

image-20211123203203004

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

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

image-20211123203418354

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

image-20211123203609958

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

image-20211123203858982

订单详情:

image-20211123203831023

数据展示 VO:

image-20211123204416320

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

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

image-20211123205148954

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

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

怎么处理呢?

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

问题二

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

浅拷贝和深拷贝

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

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

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

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

基本数据类型和引用类型

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

整型 byte、 short、int、long
浮点类型 float、double
字符型 char
逻辑型 boolean

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

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

值传递和引用传递

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

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

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

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

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

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

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

如何实现深拷贝

clone()

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

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

修改代码:

TicketOrderDetail

image-20211124102504452

TicketOrder

image-20211124102822922

测试

image-20211124102915953

结果

image-20211124102949905

但是这种做法有个弊端,这里我们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 的深拷贝和浅拷贝

猜你喜欢

转载自juejin.im/post/7111873295405809672