zoukankan      html  css  js  c++  java
  • Java对象深拷贝浅拷贝总结

    在java开发的过程中我们很多时候会有深拷贝需求,比如将一个请求体拷贝多次,修改成多个不同版笨,分别发给不同的服务,在比如维护不同的缓存时。还有些时候并不需要深拷贝,只是简单的类型转换,比如到将do对象转换为dto对象返回给前端,其中两者的字段基本相同,只是类名不一样。本文主要罗列了下自己总结的拷贝方式和适合的场景(深浅拷贝原理文章很多,本文不再解释)。

    拷贝过程中用到的Bean定义:

    
    @Data
    public class Source {
        String a;
        Filed1 filed1;
        Filed1 filed2;
        List<Filed1> fileds;
    
    <span class="hljs-meta">@NoArgsConstructor</span>
    <span class="hljs-meta">@AllArgsConstructor</span>
    <span class="hljs-meta">@Data</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Filed1</span> </span>{
        String id;
    }
    

    }

    深拷贝

    1. 手动new

        Source source = getSource();
        Source target = new Source();
        target.setFiled1(new Source.Filed1(source.getFiled1().getId()));
        target.setFiled2(new Source.Filed1(source.getFiled2().getId()));
    
    <span class="hljs-keyword">if</span> (source.getFileds() != <span class="hljs-keyword">null</span>) {
        ArrayList&lt;Source.Filed1&gt; fileds = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;(source.getFileds().size());
        <span class="hljs-keyword">for</span> (Source.Filed1 filed : source.getFileds()) {
            fileds.add(<span class="hljs-keyword">new</span> Source.Filed1(filed.getId()));
        }
        target.setFileds(fileds);
    }
    

    手动new非常简单,但是非常繁琐不利于后期的维护,每次修改类定义的时候需要修改相应的copy方法,不过性能非常高。

    2. clone方法

    
        //  Source类
        public Source clone() {
            Source clone = null;
            try {
                clone = (Source) super.clone();
                clone.setFiled1(filed1.clone());
                clone.setFiled2(filed2.clone());
                //列表的克隆
                if (fileds != null) {
                    ArrayList<Filed1> target = new ArrayList<>(this.fileds.size());
                    for (Filed1 filed : this.fileds) {
                        target.add(filed.clone());
                    }
                    clone.setFileds(target);
                }
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return clone;
        }
        // Filed1类
        public Filed1 clone() throws CloneNotSupportedException {
                return (Filed1) super.clone();
        }
    

    在重写clone方法的时候,如果类的字段类型是String和Integer等不可变类型,那么source实例对应的字段是可以复用的,以为这个字段值不能被修改。如果字段类型是可变类型则也需要重写,如Source中Filed1字段类型不是不可变类型,则也需要重写clone方法,另外注意重写clone方法的类必须实现Cloneable类(public class Source implements Serializable),否则会抛出CloneNotSupportedException。

    3. java自带序列化

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        new ObjectOutputStream(out).writeObject(source);
    
    ObjectInputStream in = <span class="hljs-keyword">new</span> ObjectInputStream(<span class="hljs-keyword">new</span> ByteArrayInputStream(out.toByteArray()));
    Source target = (Source) in.readObject();
    
    <span class="hljs-comment">// spring中封装了下可以直接使用</span>
    <span class="hljs-comment">// Source target = (Source) SerializationUtils.deserialize(SerializationUtils.serialize(source));</span>
    

    这个方法很多书中都有提起,因为序列化来实现深度拷贝代码比较简单,可扩展性好,后期添加字段无需修改实现,不过类需要继承标记接口Serializable(public class Source implements Serializable)。不过这个方法没有什么实际用途,因为确实性能非常低。

    4. json序列化

    public class JsonCopy {
        private static ObjectMapper mapper = new ObjectMapper();
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">encodeWithoutNull</span><span class="hljs-params">(Object obj)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-keyword">return</span> mapper.writeValueAsString(obj);
    }
    
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> &lt;T&gt; <span class="hljs-function">T <span class="hljs-title">decodeValueIgnoreUnknown</span><span class="hljs-params">(String str, Class&lt;T&gt; clazz)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-keyword">return</span> mapper.readValue(str, clazz);
    }
    
    
    <span class="hljs-comment">// 一千万次  15.3秒</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> &lt;T&gt; <span class="hljs-function">T <span class="hljs-title">copy</span><span class="hljs-params">(T source, Class&lt;T&gt; tClass)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-keyword">return</span> decodeValueIgnoreUnknown(encodeWithoutNull(source), tClass);
    }
    

    }

    一个简单的工具类,利用了Jackson库,性能一般,不过扩展性好,比java自带序列化很大提升。

    性能测试

    我在自己的机器上用每种方法实现Source对象的一千万次拷贝,测试了时间。结果如下:

    类型 测试结果
    手动new 一千万次 774毫秒
    clone方法 一千万次 827毫秒
    java自带序列化 一千万次 109.7秒
    json序列化 一千万次 15.3秒

    深拷贝总结

    从可扩展性和性能方面的考虑,如果注重性能,那么使用手动new和clone方法,如果注重扩展性那么使用java自带序列化和json序列化。平时的使用中,优先使用json序列化,因为大部分场景下cpu不是瓶颈,在一些热点代码中改用重写clone方法。使用clone方法和手动New两个性能和可维护性都类似,只不过看你的喜好,我是认为clone比较符合Java风格,将对象的clone方法写在那个类中。

    浅拷贝

    1. spring BeanUtils(Apache BeanUtils)

    Source source = getSource();
    Source target = new Source();
    BeanUtils.copyProperties(source, target);
    

    spring的BeanuUtils和Apache BeanUtils原理都类似,都是利用反射获取了对象的字段,逐个赋值,性能方面其实也是比较好了,虽然利用了反射,但是内部缓存了反射的结果,后面在复制的时候可以直接取缓存的结果。反射的性能损耗在获取Class信息那一块,在调用的开销和普通调用的类似,Jvm也会使用Jit进行优化。

    2. mapstruct

    
    @Mapper
    public interface SourceMapper {
    
    SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
    
    <span class="hljs-function">Source <span class="hljs-title">copy</span><span class="hljs-params">(Source car)</span></span>;
    

    }

    Source target = SourceMapper.INSTANCE.copy(source);

    mapstruct和lombok的原理类型,在编译期根据你的注解生成所需要的方法,所以他的性能理论上和手写是一样的,现在springboot也可以和他很好的结合,如果遇到了对象拷贝的性能瓶颈可以考虑用下这个类库。不过遗憾的是他并不支持深拷贝。https://github.com/mapstruct/mapstruct/issues/695

    性能测试

    类型 测试结果
    BeanUtils 一千万次 1825毫秒
    mapstruct 一千万次 235毫秒

    浅拷贝总结

    浅拷贝也可以看到可以复制不同对象的实例字段,这是序列化和Clone方法等不具备的优势,在转化Bean的时候十分有用。在一般情况下,推荐使用Spring的BeanUtils类,不用引入额外的依赖,性能也够用。如果在高并发的场景下,可以考虑通过mapstruct进行优化,两者会有一个数量级的差距。

  • 相关阅读:
    结构型模式のBridge桥梁模式
    创建型模式のBuilder建造者模式
    设计模式的一点思考
    创建型模式のAbstractFactory抽象工厂模式
    初试phoenix
    内网搭建git server
    nsq 学习(三)nsqlookupd
    nsq 学习(二)简单使用
    nsq 学习(一)源码安装nsq
    go学习实践-protobuf
  • 原文地址:https://www.cnblogs.com/edda/p/12712206.html
Copyright © 2011-2022 走看看