一劳永逸解决问题之深复制

说明

关于这个问题jdk始终没有给出官方的解决办法,第三方的包以及网上的手写代码很多时候也会出现各种各样的问题,如spring 的BeanUtils根本就无法做到完全深复制,如网上使用的Property的方式,要么是性能极差,要么是bug特别多,稍微复杂一点的对象就会报错。
其实目前来说实现深复制主流的方式大概有如下3种:
1.使用流的方式实现,比如mybatisPlus的SerializationUtils.clone
2.使用反射的方式
3.使用json序列化的方式,如fastjson和gson等,如JSON.parseObject(JSON.toJSONString(object), Xxx.class)
以上三种经过反复测试得出如下结论:
单线程执行下,数据量1万以内,第2种反射的方式性能明显优于其它两种,数据量越小性能越好,对比第1种流的方式,执行10次深复制,流的方式大概50毫秒,反射大概8毫秒,json序列化方式大概260毫秒。执行1万次,流大概400毫秒,反射大概250毫秒,序列化方式大概350毫秒。数据量1万以上,如在for循环种大量深复制对象的话,执行10万次,流的方式大概1800毫秒,反射的方式大概1300毫秒,序列化的方式大概370毫秒。由上面的数据不难看出使用第3种json序列化的方式在大数据的情况下波动性非常小,性能明显优于其它两种方式,而在数据量较少的情况下第2种反射的方式明显优于其它方式。

个人理解

虽然json序列化的方式在大数据量下有着十分明显的性能优势,但却有如下三个问题
1.使用场景较小,同一个对象的效率较高,这种情况基本上是在for循环中,且深复制在1万次以上,这样的场景基本上是不多见的
2.json的方式部分在解析时难免会出现bug,如gson进行深复制时对象如果继承另一个对象,那么会出现问题
3.json的方式在序列化时有时会出现对于is方法命名的boolean类型会解析不正确,如jackson

通过上面的分析不难看出,其实开发的时候更需要一种单次并且功能以及性能均佳的深复制方法,于是第二种通过反射的方式来实现深复制便作为了首选。毕竟反射执行10次的时间仅仅只需要8毫秒,而无论是流还是json目前都做不到这一点,且后续还可以通过判断是否大于1万实现动态切换深复制的算法来达到一个性能上的平衡。因此基于这些原因,我将json-script-rule框架中的代码摘出来并用反射的方式来实现深复制,看到的可以直接拿过去用。

/**
     * <p>深拷贝,支持不同类型的对象间属性拷贝,支持各种容器类型包括数组,集合,Map的拷贝,支持对象中各种复杂嵌套类型拷贝,支持对象继承关系拷贝,性能优于序列化方式1倍
     * <p>支持复杂嵌套类型,如LinkedList<Object>;Set<Map<String,Integer>>;LinkedHashMap<String,Map<String,Integer>>;
     * <p>注意:字段名称相同才会进行拷贝,当目标对象属性无法在来源对象中找到,或者两种类型无法强转,又或者是其它类型出错时将不进行复制,结果为默认值
     * <p>注意:任何待copy的对象或其子类对象都应包含一个无参的构造函数,否则将返回null值,来源对象的子类和父亲属性名重复时将基于多态方式以子类属性的值进行拷贝
     * <p>注意:由于采用对象内同名属性拷贝策略,因此一定要避免两边对象的类型差距过大,如target为普通对象而source为Map类型,这样拷贝出来的结果容易引发未知的问题
     * @param target 目标对象类型,当来源对象的类型为目标对象的子类型时将会以多态的子类型进行复制
     * @param source 属性来源对象
     * @return 返回目标类型的复制后的对象
     * */
    @SuppressWarnings("unchecked")
    public static <T> T copy(Class<T> target, Object source) {
        if (source == null || target == null){
            return null;
        }
        try{
            if (isPrimitiveType(target) || target.isEnum()){
                return (T) source;
            }else if (Date.class == target){
                return (T) ((Date)source).clone();
            }else if (LocalDateTime.class == target){
                LocalDateTime ldt = (LocalDateTime)source;
                return (T) LocalDateTime.of(ldt.toLocalDate(),ldt.toLocalTime());
            }else if (BigDecimal.class == target){
                return (T) new BigDecimal(((BigDecimal)source).toPlainString());
            }else if (Collection.class.isAssignableFrom(target)){
                return (T) copyCollection((Collection<?>)source);
            }else if (Map.class.isAssignableFrom(target)){
                return (T) copyMap((Map<?,?>)source);
            }else if (target.isArray()){
                return (T) copyArray(source);
            }else{
                if (isJdkType(target)){
                    throw new Exception("not supported this type->"+target.getSimpleName());
                }
                Class<?> sourceType = source.getClass();
                T t;
                if (target.isAssignableFrom(sourceType)){
                    t = (T) sourceType.getConstructor().newInstance();
                }else{
                    t = target.getConstructor().newInstance();
                }
                Field s;
                for (Field f:target.getDeclaredFields()){
                    try{
                        s = sourceType.getDeclaredField(f.getName());
                        f.setAccessible(true);
                        s.setAccessible(true);
                        f.set(t, copy(f.getType(),s.get(source)));
                    }catch (Exception ignored) {}
                }
                return t;
            }
        }catch (Exception e) {
            return null;
        }
    }

主流程的代码可以按照需求来进行更改,比如上面的Bigdecimal的判断,不过这已经能够满足绝大部分的需求了,以下是主流程所依赖的其它抽离出来的方法

public static boolean isPrimitiveType(Class<?> clazz){
    return clazz==String.class || clazz.isPrimitive() || isPrimitiveWrapper(clazz);
}
public static boolean isPrimitiveWrapper(Class<?> clazz){
    try {
      return ((Class<?>) clazz.getField("TYPE").get(null)).isPrimitive();
    } catch (Exception e) {
        return false;
    }
}
public static <E> Collection<E> copyCollection(Collection<?> source){
        try {
            Collection<E> collection = source.getClass().getConstructor().newInstance();
            for (E e:(Collection<E>)source) {
                collection.add((E) copy(e.getClass(),e));
            }
            return collection;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
}
public static <K,V> Map<K,V> copyMap(Map<?,?> source){
        try {
            Map<K,V> map = source.getClass().getConstructor().newInstance();
            source.forEach((k,v)->map.put((K) k, (V) copy(v.getClass(),v)));
            return map;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
}
public static Object copyArray(Object source){
        int length = Array.getLength(source);
        Class<?> type = source.getClass().getComponentType();
        Object array = Array.newInstance(type,length);
        for (int i=0;i<length;i++){
            Array.set(array,i, copy(type,Array.get(source,i)));
        }
        return array;
    }
public static boolean isJdkType(Class<?> type){
        return type.getClassLoader()==null;
    }

以上就是关于深复制的全部代码了,其实代码十分的简洁易懂,简约而不简单,基本上能够满足你全部需求了


此后会陆续从json-script-rule中摘取相对较好的工具方法分享给大家

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容