在Java开发工作中,有很多时候我们需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息。这两个对象实例有可能是同一个类的两个实例,也可能是不同类的两个实例,但是他们的属相名称相同。例如DO、DTO、VO、DAO等,这些实体的意义请查看DDD中分层架构。本文主要介绍几种对象拷贝的方法

1. 对象拷贝

对象拷贝分为深拷贝和浅拷贝。根据使用场景进行不同选择。在Java中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。

深度拷贝和浅度拷贝的主要区别在于是否支持引用类型的属性拷贝,本文将探讨目前使用较多的几种对象拷贝的方案,以及其是否支持深拷贝和性能对比。

2. BeanUtils

2.1 apache的BeanUtils方案

使用org.apache.commons.beanutils.BeanUtils进行对象深入复制时候,主要通过向BeanUtils框架注入新的类型转换器,因为默认情况下,BeanUtils对复杂对象的复制是引用,例如:

public static void beanUtilsTest() throws Exception {// 注册转化器BeanUtilsBean.getInstance().getConvertUtils().register(new ArbitrationConvert(), ArbitrationDO.class);Wrapper wrapper = new Wrapper();wrapper.setName("copy");wrapper.setNameDesc("copy complex object!");wrapper.setArbitration(newArbitrationDO());Wrapper dest = new Wrapper();// 对象复制BeanUtils.copyProperties(dest, wrapper);// 属性验证wrapper.getArbitration().setBizId("1");System.out.println(wrapper.getArbitration() == dest.getArbitration());System.out.println(wrapper.getArbitration().getBizId().equals(dest.getArbitration().getBizId()));
}public class ArbitrationConvert implements Converter {@Overridepublic <T> T convert(Class<T> type, Object value) {if (ArbitrationDO.class.equals(type)) {try {return type.cast(BeanUtils.cloneBean(value));} catch (Exception e) {e.printStackTrace();}}return null;}
}

可以发现,使用org.apache.commons.beanutils.BeanUtils复制引用时,主和源的引用为同一个,即改变了主的引用属性会影响到源的引用,所以这是一种浅拷贝。

需要注意的是,apache的BeanUtils中,以下类型如果为空,会报错(org.apache.commons.beanutils.ConversionException: No value specified for  *)

/*** Register the converters for other types.* </p>* This method registers the following converters:* <ul>*     <li>Class.class - {@link ClassConverter}*     <li>java.util.Date.class - {@link DateConverter}*     <li>java.util.Calendar.class - {@link CalendarConverter}*     <li>File.class - {@link FileConverter}*     <li>java.sql.Date.class - {@link SqlDateConverter}*     <li>java.sql.Time.class - {@link SqlTimeConverter}*     <li>java.sql.Timestamp.class - {@link SqlTimestampConverter}*     <li>URL.class - {@link URLConverter}* </ul>* @param throwException <code>true if the converters should* throw an exception when a conversion error occurs, otherwise <code>* <code>false if a default value should be used.*/private void registerOther(boolean throwException) {register(Class.class,         throwException ? new ClassConverter()        : new ClassConverter(null));register(java.util.Date.class, throwException ? new DateConverter()        : new DateConverter(null));register(Calendar.class,      throwException ? new CalendarConverter()     : new CalendarConverter(null));register(File.class,          throwException ? new FileConverter()         : new FileConverter(null));register(java.sql.Date.class, throwException ? new SqlDateConverter()      : new SqlDateConverter(null));register(java.sql.Time.class, throwException ? new SqlTimeConverter()      : new SqlTimeConverter(null));register(Timestamp.class,     throwException ? new SqlTimestampConverter() : new SqlTimestampConverter(null));register(URL.class,           throwException ? new URLConverter()          : new URLConverter(null));}

当遇到这种问题是,可以手动将类型转换器注册进去,比如data类型:

public class BeanUtilEx extends BeanUtils { private static Map cache = new HashMap();
private static Log logger = LogFactory.getFactory().getInstance(BeanUtilEx.class); private BeanUtilEx() {
} static {
// 注册sql.date的转换器,即允许BeanUtils.copyProperties时的源目标的sql类型的值允许为空
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlDateConverter(null), java.sql.Date.class);
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlDateConverter(null), java.util.Date.class);
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlTimestampConverter(null), java.sql.Timestamp.class);
// 注册util.date的转换器,即允许BeanUtils.copyProperties时的源目标的util类型的值允许为空
} public static void copyProperties(Object target, Object source)
throws InvocationTargetException, IllegalAccessException {
// 支持对日期copy
org.apache.commons.beanutils.BeanUtils.copyProperties(target, source); } 

2.2 apache的PropertyUtils方案

PropertyUtils的copyProperties()方法几乎与BeanUtils.copyProperties()相同,主要的区别在于后者提供类型转换功能,即发现两个JavaBean的同名属性为不同类型时,在支持的数据类型范围内进行转换,PropertyUtils不支持这个功能,所以说BeanUtils使用更普遍一点,犯错的风险更低一点。而且它仍然属于浅拷贝。

Apache提供了 SerializationUtils.clone(T),T对象需要实现 Serializable 接口,他属于深克隆。

2.3 spring的BeanUtils方案

Spring中的BeanUtils,其中实现的方式很简单,就是对两个对象中相同名字的属性进行简单get/set,仅检查属性的可访问性。

public static void copyProperties(Object source, Object target) throws BeansException {copyProperties(source, target, (Class)null, (String[])null);}public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {copyProperties(source, target, editable, (String[])null);}public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {copyProperties(source, target, (Class)null, ignoreProperties);}private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) throws BeansException {Assert.notNull(source, "Source must not be null");Assert.notNull(target, "Target must not be null");Class actualEditable = target.getClass();if(editable != null) {if(!editable.isInstance(target)) {throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");}actualEditable = editable;}PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);List ignoreList = ignoreProperties != null?Arrays.asList(ignoreProperties):null;PropertyDescriptor[] var7 = targetPds;int var8 = targetPds.length;for(int var9 = 0; var9 < var8; ++var9) {PropertyDescriptor targetPd = var7[var9];Method writeMethod = targetPd.getWriteMethod();if(writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());if(sourcePd != null) {Method readMethod = sourcePd.getReadMethod();if(readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {try {if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {readMethod.setAccessible(true);}Object ex = readMethod.invoke(source, new Object[0]);if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}writeMethod.invoke(target, new Object[]{ex});} catch (Throwable var15) {throw new FatalBeanException("Could not copy property \'" + targetPd.getName() + "\' from source to target", var15);}}}}}}

可以看到, 成员变量赋值是基于目标对象的成员列表, 并且会跳过ignore的以及在源对象中不存在的, 所以这个方法是安全的, 不会因为两个对象之间的结构差异导致错误, 但是必须保证同名的两个成员变量类型相同.

3. dozer

Dozer(http://dozer.sourceforge.net/)能够实现深拷贝。Dozer是基于反射来实现对象拷贝,反射调用set/get 或者是直接对成员变量赋值 。 该方式通过invoke执行赋值,实现时一般会采用beanutil, Javassist等开源库。

简单引用网上的例子,大多都是基于xml的配置,具体请查看其它Blog:

package com.maven.demo;import java.util.HashMap;
import java.util.Map;import org.dozer.DozerBeanMapper;
import org.junit.Test;import static org.junit.Assert.assertEquals;public class Demo{/*** map->bean*/@Testpublic void testDozer1() {Map<String,Object> map = new HashMap();map.put("id", 10000L);map.put("name", "小兵");map.put("description", "帅气逼人");DozerBeanMapper mapper = new DozerBeanMapper();ProductVO product = mapper.map(map, ProductVO.class);assertEquals("小兵",product.getName());assertEquals("帅气逼人",product.getDescription());assertEquals(Long.valueOf("10000"), product.getId());}/*** VO --> Entity  (不同的实体之间,不同的属性字段进行复制)*/@Testpublic void testDozer2(){ProductVO product = new ProductVO();product.setId(10001L);product.setName("xiaobing");product.setDescription("酷毙了");DozerBeanMapper mapper = new DozerBeanMapper();ProductEntity productEntity = mapper.map(product, ProductEntity.class);assertEquals("xiaobing",productEntity.getProductName());}}

4.  MapStrcut

MapStrcut属于编译期的对象复制方案,它能够动态生成set/get代码的class文件 ,在运行时直接调用该class文件。该方式实际上扔会存在set/get代码,只是不需要自己写了。

@Mapper(componentModel = "spring")
public interface MonitorAppGroupIdcDTOMapper {MonitorAppGroupIdcDTOMapper MAPPER = Mappers.getMapper(MonitorAppGroupIdcDTOMapper.class);void mapping(MonitorAppGroupIdcDTO source, @MappingTarget MonitorAppGroupIdcDTO dest);
}

5. 自定义Pojoconvert

public J copyPojo( P src, J des) throws NoSuchMethodException,SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {if(src == null || des==null){return null;}String name = null ;String sufix = null;Class<?> cls = des.getClass() ;Method[] methods = cls.getMethods();for(Method m: methods){name = m.getName();if(name!=null && name.startsWith("set") && m.getParameterTypes().length==1){sufix = name.substring(3);m.getParameterTypes() ;Method getM = cls.getMethod("get"+sufix);m.invoke(des, getM.invoke(src));}}return des ;
}

没有那么多验证,不是很安全但是性能不错。

6. BeanCopier

@Testpublic void test_convert_entity_to_model_performance_use_beancopier(){List<ShopCouponEntity> entityList  = ...long start = System.currentTimeMillis();BeanCopier b = BeanCopier.create(ShopCouponEntity.class, ShopCouponModel.class, false);List<ShopCouponModel> modelList = new ArrayList<>();for (ShopCouponEntity src : entityList) {ShopCouponModel dest = new ShopCouponModel();b.copy(src, dest, null);modelList.add(dest);}System.out.printf("BeanCopier took time: %d(ms)%n",System.currentTimeMillis() - start);}

可以通过缓存BeanCopier的实例来提高性能。

BeanCopier b = getFromCache(sourceClass,targetClass); //从缓存中取long start = System.currentTimeMillis();List<ShopCouponModel> modelList = new ArrayList<>();for (ShopCouponEntity src : entityList) {ShopCouponModel dest = new ShopCouponModel();b.copy(src, dest, null);modelList.add(dest);}

7. fastjson和GSON

使用fastjson和GSON主要是通过对象json序列化和反序列化来完成对象复制,这里只是提供一种不一样的对象拷贝的思路,例子略。

8. 性能

对两种BeanUtils、Gson以及自定义Pojoconvert测试了性能

NewNovelMode des = null ;
NewNovelMode ori = buildModel();
Gson gson = new Gson();
int count = 100000;
//org.springframework.beans.BeanUtils.copyProperties
long s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = new NewNovelMode();org.springframework.beans.BeanUtils.copyProperties(ori, des);
}
System.out.println("springframework BeanUtils cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));//org.apache.commons.beanutils.BeanUtils
s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = new NewNovelMode();org.apache.commons.beanutils.BeanUtils.copyProperties(des, ori);
}
System.out.println("apache BeanUtils cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));//gson转换
s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = gson.fromJson(gson.toJson(ori), NewNovelMode.class);
}
System.out.println("gson cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));//Pojo转换类
s = System.currentTimeMillis();
PojoUtils<NewNovelMode, NewNovelMode> pojoUtils = new PojoUtils<NewNovelMode, NewNovelMode>();
for(int i=0;i<count;i++){des = new NewNovelMode();pojoUtils.copyPojo(ori,des);
}
System.out.println("Pojoconvert cost:"+(System.currentTimeMillis() - s));
//      System.out.println(new Gson().toJson(des));

结果就不贴出来了,在这里总结一下

Spring的BeanUtils比较稳定,不会因为量大了,耗时明显增加,但其实基准耗时比较长;apache的BeanUtils稳定性与效率都不行,不可取;Gson,因为做两个gson转换,所以正常项目中,可能耗时会更少一些;PojoUtils稳定不如spring,但是总耗时优势明显,原因是它只是根据项目的需求,实现的简单的转换模板,这个代码在其它的几个工具类均有。

而在网上的其他Blog中(参见Reference),对Apache的BeanUtils、PropertyUtils和CGLIB的BeanCopier作了性能测试。

测试结果:

性能对比: BeanCopier > BeanUtils. 其中BeanCopier的性能高出另外两个100数量级。

综上推荐使用:

1. BeanUtils(简单,易用)

2. BeanCopier(加入缓存后和手工set的性能接近)

3. Dozer(深拷贝)

4. fastjson(特定场景下使用)

转自:https://my.oschina.net/hosee/blog/1483965

谈谈Java开发中的对象拷贝相关推荐

  1. 《Android游戏开发详解》一2.18 使用Java API中的对象

    本节书摘来异步社区<Android游戏开发详解>一书中的第2章,第2.18节,译者: 李强 责编: 陈冀康,更多章节内容可以访问云栖社区"异步社区"公众号查看. 2.1 ...

  2. Java开发中常见的危险信号

    Dustin Marx是一位专业软件开发者,从业已经有17年的时间,他拥有电子工程学士学位,还是一位MBA.Dustin维护着一个博客,专门介绍软件开发的各个主题.近日,他撰文谈到了Java开发中常见 ...

  3. 编写高质量代码:改善Java程序的151个建议 (第1章 Java开发中通用的方法和准则)

    第1章 Java开发中通用的方法和准则 The reasonable man adapts himself to the world;the unreasonable one persists in ...

  4. 谈谈WEB开发中的苦大难字符集问题

    记得刚做javaweb开发的时候被这个编码问题搞得晕头转向,经常稀里糊涂的编码正常了一会编码又乱了.那个时候迫于项目进度大多都是知其然不知其所以然.后来有时间就把整个体系搞了个遍,终于摸通了来龙去脉. ...

  5. Java开发中文件读取方式总结

    JAVA开发中,免不了要读文件操作,读取文件,首先就需要获取文件的路径.路径分为绝对路径和相对路径. 在文件系统中,绝对路径都是以盘符开始的,例如C:abc1.txt. 什么是相对路径呢?相对路径就是 ...

  6. Java开发中更多常见的危险信号

    在< Java开发中的常见危险信号>一文中,我研究了一些不一定本身就是错误或不正确的做法,但它们可能表明存在更大的问题. 这些"红色标记"类似于"代码气味&q ...

  7. Java开发中的常见危险信号

    在开发,阅读,复审和维护成千上万行Java代码的几年中,我已经习惯于看到Java代码中的某些" 危险信号 ",这些信号通常(但可能并非总是)暗示着代码问题. 我不是在谈论总是错误的 ...

  8. Java开发中常用的设计模式-单例模式

    单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式. Java开发中常用的设计模式-单例模式 单例模式有3个特点: 单例类只有一个实例对象: 该单例对象必须 ...

  9. Javascript中的对象拷贝(对象复制/克隆)

    Javascript中的对象拷贝(对象复制/克隆) 李俊才 CSDN:jcLee95 邮箱:291148484@163.com 1. 对象的引用 要说"拷贝"还要先说"引 ...

最新文章

  1. 记录遇到的Altium designer显示布线未完成坑
  2. 实战 | 利用Delta Lake使Spark SQL支持跨表CRUD操作
  3. mysql的order by,group by和distinct优化
  4. python 比较文件不同,在python中逐行比较两个不同的文件
  5. 利用javascript动态创建表格
  6. 通过修改word文件,来屏蔽宏代码
  7. 线性调频信号的时频域分析
  8. 服务器没有D盘怎么架设传奇?
  9. 【Python】使用python 画出一张机器猫doraemon
  10. linux asm 裸设备,为ASM生成裸设备
  11. Windows防火墙的应用
  12. git 调换提交顺序
  13. 怎样调整Firefox火狐浏览器开发者控制台字体大小
  14. 拆长虹iho3000_(CA版)四川长虹iho-3000t晶晨s905l-b刷全网通系统教程可救砖头
  15. 机器学习项目实战----新闻分类任务(二)
  16. MySQL趋势与前景技术分享
  17. python可视化神器——pyecharts(词云图雷达图极坐标系)
  18. Spark项目之电商用户行为分析大数据平台之(三)大数据集群的搭建
  19. Unity 绿幕视频抠图算法原理与实现 -- 效果极好
  20. dataworks函数

热门文章

  1. cad在线转低版本_为什么别人制图那么快?41个CAD实用技巧,3天轻松玩转CAD
  2. 微软的平板电脑_Microsoft 微软 Surface Go 2 10.5英寸二合一平板电脑(m3-8100Y、8GB、128GB、LTE) 5788元...
  3. 非阻IO与EWOULDBLOCK EAGAIN
  4. python钻石数据分析_数据分析该用什么工具?
  5. 【转】PF_RING开发指南
  6. 【转】UML基础: 第 2 部分 - 对象图 (Object Diagram)
  7. 第六节:WebApi的部署方式(自托管)
  8. 【转】DevOps到底是什么意思?
  9. oracle临时表空间组,证明临时表空间组在并发session时的作用
  10. python文本去重函数_python3.4.3下逐行读入txt文本并去重的方法