这样的问题代码,我实习的时候都写不出来!

开发 开发工具
本文的内容是最近我刚刚遇到的一个问题,问题代码是我自己写的,也是我自己写单元测试的时候发现的,也是我自己修复的,修复完之后,我反思了一下:这样的问题代码,我实习的时候都写不出来。

[[413038]]

本文的内容是最近我刚刚遇到的一个问题,问题代码是我自己写的,也是我自己写单元测试的时候发现的,也是我自己修复的,修复完之后,我反思了一下:这样的问题代码,我实习的时候都写不出来。

可是为什么我就写出来了呢?其实还是因为有些知识没那么扎实了~就容易被忽略了,于是我在团队群里面强调了一下这个问题:

所以,本文主要是关于BeanUtils工具的属性拷贝以及深拷贝、浅拷贝等问题的。好了开始正文,介绍下问题代码是什么,为什么有问题,又符合修改?

在日常开发中,我们经常需要给对象进行赋值,通常会调用其set/get方法,有些时候,如果我们要转换的两个对象之间属性大致相同,会考虑使用属性拷贝工具进行。

如我们经常在代码中会对一个数据结构封装成DO、SDO、DTO、VO等,而这些Bean中的大部分属性都是一样的,所以使用属性拷贝类工具可以帮助我们节省大量的set和get操作。

市面上有很多类似的工具类,比较常用的有

1、Spring BeanUtils

2、Cglib BeanCopier

3、Apache BeanUtils

4、Apache PropertyUtils

5、Dozer

6、MapStucts

这里面我比较建议大家使用的是MapStructs,我在《丢弃掉那些BeanUtils工具类吧,MapStruct真香!!!》中介绍过原因。这里就不再赘述了。

最近我们有个新项目,要创建一个新的应用,因为我自己分析过这些工具的效率,也去看过他们的实现原理,比较下来之后,我觉得MapStruct是最适合我们的,于是就在代码中引入了这个框架。

另外,因为Spring的BeanUtils用起来也比较方便,所以,代码中对于需要beanCopy的地方主要在使用这两个框架。

我们一般是这样的,如果是DO和DTO/Entity之间的转换,我们统一使用MapStruct,因为他可以指定单独的Mapper,可以自定义一些策略。

如果是同对象之间的拷贝(如用一个DO创建一个新的DO),或者完全不相关的两个对象转换,则使用Spring的BeanUtils。

刚开始都没什么问题,但是后面我在写单测的时候,发现了一个问题。

问题

先来看看我们是在什么地方用的Spring的BeanUtils

我们的业务逻辑中,需要对订单信息进行修改,在更改时,不仅要更新订单的上面的属性信息,还需要创建一条变更流水。

而变更流水中同时记录了变更前和变更后的数据,所以就有了以下代码:

  1. //从数据库中查询出当前订单,并加锁 
  2.  
  3. OrderDetail orderDetail = orderDetailDao.queryForLock(); 
  4.  
  5.  
  6. //copy一个新的订单模型 
  7.  
  8. OrderDetail newOrderDetail = new OrderDetail(); 
  9.  
  10. BeanUtils.copyProperties(orderDetail, newOrderDetail); 
  11.  
  12.  
  13. //对新的订单模型进行修改逻辑操作 
  14.  
  15. newOrderDetail.update(); 
  16.  
  17.  
  18. //使用修改前的订单模型和修改后的订单模型组装出订单变更流水 
  19.  
  20. OrderDetailStream orderDetailStream = new OrderDetailStream(); 
  21.  
  22. orderDetailStream.create(orderDetail, newOrderDetail); 

大致逻辑是这样的,因为创建订单变更流水的时候,需要一个改变前的订单和改变后的订单。所以我们想到了要new一个新的订单模型,然后操作新的订单模型,避免对旧的有影响。

但是,就是这个BeanUtils.copyProperties的过程其实是有问题的。

因为BeanUtils在进行属性copy的时候,本质上是浅拷贝,而不是深拷贝。

浅拷贝?深拷贝?

什么是浅拷贝和深拷贝?来看下概念。

1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

我们举个实际例子,来看下为啥我说BeanUtils.copyProperties的过程是浅拷贝。

先来定义两个类:

  1. public class Address { 
  2.  
  3.     private String province; 
  4.  
  5.     private String city; 
  6.  
  7.     private String area; 
  8.  
  9.     //省略构造函数和setter/getter 
  10.  
  11.  
  12.  
  13. class User { 
  14.  
  15.     private String name
  16.  
  17.     private String password
  18.  
  19.     private Address address; 
  20.  
  21.     //省略构造函数和setter/getter 
  22.  

然后写一段测试代码:

  1. User user = new User("Hollis""hollischuang"); 
  2.  
  3. user.setAddress(new Address("zhejiang""hangzhou""binjiang")); 
  4.  
  5.  
  6. User newUser = new User(); 
  7.  
  8. BeanUtils.copyProperties(user, newUser); 
  9.  
  10. System.out.println(user.getAddress() == newUser.getAddress()); 

以上代码输出结果为:true

即,我们BeanUtils.copyProperties拷贝出来的newUser中的address对象和原来的user中的address对象是同一个对象。

可以尝试着修改下newUser中的address对象:

  1. newUser.getAddress().setCity("shanghai"); 
  2.  
  3.  System.out.println(JSON.toJSONString(user)); 
  4.  
  5.  System.out.println(JSON.toJSONString(newUser)); 

输出结果:

  1. {"address":{"area":"binjiang","city":"shanghai","province":"zhejiang"},"name":"Hollis","password":"hollischuang"
  2.  
  3. {"address":{"area":"binjiang","city":"shanghai","province":"zhejiang"},"name":"Hollis","password":"hollischuang"

可以发现,原来的对象也受到了修改的影响。

这就是所谓的浅拷贝!

如何进行深拷贝

发现问题之后,我们就要想办法解决,那么如何实现深拷贝呢?

1、实现Cloneable接口,重写clone()

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

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

将上述代码修改下,重写clone方法:

  1. public class Address implements Cloneable{ 
  2.  
  3.     private String province; 
  4.  
  5.     private String city; 
  6.  
  7.     private String area; 
  8.  
  9.     //省略构造函数和setter/getter 
  10.  
  11.  
  12.  
  13.     @Override 
  14.  
  15.     public Object clone() throws CloneNotSupportedException { 
  16.  
  17.         return super.clone(); 
  18.  
  19.     } 
  20.  
  21.  
  22.  
  23. class User implements Cloneable{ 
  24.  
  25.     private String name
  26.  
  27.     private String password
  28.  
  29.     private Address address; 
  30.  
  31.     //省略构造函数和setter/getter 
  32.  
  33.  
  34.     @Override 
  35.  
  36.     protected Object clone() throws CloneNotSupportedException { 
  37.  
  38.         User user = (User)super.clone(); 
  39.  
  40.         user.setAddress((Address)address.clone()); 
  41.  
  42.         return user
  43.  
  44.     } 
  45.  

之后,在执行一下上面的测试代码,就可以发现,这时候newUser中的address对象就是一个新的对象了。

这种方式就能实现深拷贝,但是问题是如果我们在User中有很多个对象,那么clone方法就写的很长,而且如果后面有修改,在User中新增属性,这个地方也要改。

那么,有没有什么办法可以不需要修改,一劳永逸呢?

2、序列化实现深拷贝

我们可以借助序列化来实现深拷贝。先把对象序列化成流,再从流中反序列化成对象,这样就一定是新的对象了。

序列化的方式有很多,比如我们可以使用各种JSON工具,把对象序列化成JSON字符串,然后再从字符串中反序列化成对象。

如使用fastjson实现:

  1. User newUser = JSON.parseObject(JSON.toJSONString(user), User.class); 

也可实现深拷贝。

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

我们需要修改下上面的User和Address类,使他们实现Serializable接口,否则是无法进行序列化的。

  1. class User implements Serializable 
  2.  
  3. class Address implements Serializable 

然后在需要拷贝的时候:

  1. User newUser = (User) SerializationUtils.clone(user); 

同样,也可以实现深拷贝啦~!

总结

当我们使用各类BeanUtils的时候,一定要注意是浅拷贝还是深拷贝,浅拷贝的结果就是两个对象中的引用对象都是同一个地址,只要发生改变,都会有影响。

想要实现深拷贝,有很多种办法,其中比较常用的就是实现Cloneable接口重写clone方法,还有使用序列化+反序列化创建新对象。

好了,以上就是今天的全部内容了。

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2020-05-29 10:18:58

python开发代码

2023-02-07 13:51:11

SQLupdate语句

2021-10-07 23:13:25

手机性能技术

2021-09-14 07:06:12

程序员拷贝代码

2021-01-21 08:04:39

数据结构

2022-06-01 11:52:42

网站客户端网络

2023-09-21 15:20:49

算法开发

2009-01-20 13:57:02

网络硬盘Google云计算

2023-11-07 08:36:34

JavaScriptJS挑战

2020-08-14 07:42:33

数据库加密脱库

2021-06-04 05:19:57

运营商用户投诉

2013-01-30 12:12:20

微信二维码智能手机

2022-04-11 11:38:44

Python代码游戏

2024-01-18 13:36:00

RustGo开发

2020-10-10 09:09:21

CTOCRUD设计

2013-09-12 10:12:49

Mozilla实习

2021-12-15 23:37:04

iOS苹果系统

2012-12-13 14:52:10

2018-03-24 22:05:13

简历面试招聘

2020-02-20 10:45:57

代码JS开发
点赞
收藏

51CTO技术栈公众号