文档章节

谈谈Java开发中的对象拷贝

Hosee
 Hosee
发布于 2017/07/23 20:49
字数 2221
阅读 410
收藏 4

在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 {

    @Override
    public <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

Dozerhttp://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
     */
    @Test
    public 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  (不同的实体之间,不同的属性字段进行复制)
     */
    @Test
    public 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

@Test
    public 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(特定场景下使用)

Reference:

1. http://blog.csdn.net/yemou_blog/article/details/50292237

2. http://blog.csdn.net/caokai1992/article/details/50506252

3. http://guying1028.iteye.com/blog/996869

4. http://www.cnblogs.com/baizhanshi/p/6096810.html

5. http://www.voidcn.com/blog/xtqve/article/p-4829829.html

6. http://blog.csdn.net/paincupid/article/details/71247255

7. https://segmentfault.com/a/1190000006922799

© 著作权归作者所有

共有 人打赏支持
Hosee
粉丝 554
博文 132
码字总数 207228
作品 0
杭州
程序员
私信 提问
BATJ等大厂最全经典面试题分享

金九银十,又到了面试求职高峰期,最近有很多网友都在求大厂面试题。正好我之前电脑里面有这方面的整理,于是就发上来分享给大家。 这些题目是网友去百度、蚂蚁金服、小米、乐视、美团、58、...

老道士
09/26
0
0
【Java学习路线】新手该如何一步步的学习 Java

新手该如何一步步的学习 Java? 如果真的想学Java,最好要循序渐进,有章有法的学习它! 今天小慕就不说一些学习方法和技巧了,直接来谈每个阶段要学习的内容。 首先,给大家分享一张以 企业...

Eddie_yang
今天
0
0
叮!您收到一份超值Java基础入门资料!

摘要:Java语言有什么特点?如何最大效率的学习?深浅拷贝到底有何区别?阿里巴巴高级开发工程师为大家带来Java系统解读,带你掌握Java技术要领,突破重点难点,入门面向对象编程,以详细示例...

聒小小噪
05/12
0
0
Java学习---Java简单认识

前言 小编在学习Java方面的基础知识,发现里面有很多是结合之前的语言的特点发展过来的,不同的地方是,Java有它自己的发展和特点。下面小编先简单地做一下总结,结合看过的1-2章的J2SE视频,...

m18633778874
04/01
0
0
JNI开发流程与引用数据类型的处理

今天我们来看下Java JNI,先看下维基百科给的定义, JNI, Java Native Interface, Java本地接口,是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用或库,也可以被其他程序调用。...

juexingzhe
05/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

mysql load data 批量导入使用

最近在研究使用mycat,其中有使用批量数据导入,简单记录一下使用方式。 查看系统属性 mysql>show variables like '%secure%'+--------------------------+-----------------------+| Vari...

FansinZhao
8分钟前
0
0
浅谈React的最大亮点——虚拟DOM

在Web开发中,需要将数据的变化实时反映到UI上,这时就需要对DOM进行操作,但是复杂或频繁的DOM操作通常是性能瓶颈产生的原因,为此,React引入了虚拟DOM(Virtual DOM)的机制。 一、什么是...

peakedness丶
11分钟前
0
0
下一代大数据处理引擎,阿里云实时计算独享模式重磅发布

摘要: 11月14日,阿里云重磅发布了实时计算独享模式,即用户独享一部分物理资源,这部分资源在网络/磁盘/CPU/内存等资源上跟其他用户完全独立,是实时计算在原有共享模式基础上的重大升级。...

阿里云官方博客
16分钟前
0
0
Spring MVC 测试样例

1、需要javax.servlet-api V3.0以上支持 <!--3.0以上版本--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-......

jcc_codingBoy
18分钟前
2
0
kettle增量同步oracle数据到mysql

kettle增量同步oracle数据到mysql 适合表中有更新时间的字段 kettle安装 绿色,下载后解压即可(略), -> Download 连接数据库 需要导入jdbc相关驱动jar包到lib目录(oracle、mysql对应jar...

o00o
22分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部