Java对象拷贝的那些事以及必坑指南

1.背景

        在工作中经常会涉及到对象的复制和拷贝。特别是在调用外部的RPC接口时,经常不会直接使用调用结果的dto,而是按照需要自己定义的需要的字段,包装成新的dto,用于后续的业务,进行逻辑处理。在这中间就涉及到对象复制的问题。不仅仅局限于调用远程接口,在实际的开发中的一些增删改查操作,也涉及到对对象复制的操作。总之,对象复制是在工作中非常常用的一个操作。需要我们重视。

2.相关概念介绍

      浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。

      深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。这个方式称为深拷贝

      也就是说浅拷贝只复制一个对象,传递引用,不能复制实例。而深拷贝对对象内部的引用均复制,它是创建一个新的实例,并且复制实例。

      对于浅拷贝当对象的成员变量是基本数据类型时,两个对象的成员变量已有存储空间,赋值运算传递值,所以浅拷贝能够复制实例。但是当对象的成员变量是引用数据类型时,就不能实现对象的复制了。

3.深拷贝和浅拷贝的常见做法

3.1 深拷贝的常见做法

1.通过JAVA自带的cloneable接口,重写clone方法进行实现(不推荐)

注意点:① 如果需要克隆的对象中包含引用类型(String除外),比如:自定义的对象,那么就需要注意如下两点

  • 该成员实现Cloneable接口并覆盖clone()方法,并将类的权限提升为public。
  • 同时,修改被复制类的clone()方法,增加成员的克隆逻辑。

            ② 如果被复制对象不是直接继承Object,中间还有其它继承层次,每一层super类都需要实现Cloneable接口并覆盖clone()方法。与对象成员不同,继承关系中的clone不需要被复制类的clone()做多余的工作。

缺点:对于有大量vo,po的工程,这样做无疑增加了开发量。

2.手动New对象的方法(不推荐)

原理:人工构建对象,如果需要复制的对象中包含非基本类型,如List,对象等结构时,可以在需要的时候手动new对象,将属性值挨个调用set方法,比较繁琐,但无疑是最高效的做法

3.利用序列化的方式实现深拷贝

原理:在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。

注意点:

  • 对于被复制对象的继承链、引用链上的每一个对象都实现java.io.Serializable接口。
  • 效率与对象使用的序列化方式有关,常用的序列化

工具类:

public class SerializableCloneUtil {

    public static <T extends Serializable> T clone(T obj) {

        T cloneObj = null;

        try {

            //写入字节流
 ByteArrayOutputStream out = new ByteArrayOutputStream();

            ObjectOutputStream obs = new ObjectOutputStream(out);

            obs.writeObject(obj);

            obs.close();

           //分配内存,写入原始对象,生成新对象
 ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);

           //返回生成的新对象
 cloneObj = (T) ois.readObject();
            ois.close();

        } catch (Exception e) {

            e.printStackTrace();

        }
        return cloneObj;
    }
    
}

4.利用Json转换法

原理:可以先将对象转化为JSON,再序列化为对象,和第一种方法类似。Json转换工具可以用Jackson或者Json-lib

代表:JSON.parseObject(JSON.toJSONString(xxx), xxx.class);

5.利用Dozer实现深拷贝

1.Dozer简介:   Dozer 是一个对象转换工具。

  •   Dozer可以在JavaBean到JavaBean之间进行递归数据复制,并且这些JavaBean可以是不同的复杂的类型。
  •   所有的mapping,Dozer将会很直接的将名称相同的fields进行复制,如果field名不同,或者有特别的对应要求,则可以在xml中进行定义。也
  •       可以与Spring进行整合:Dozer - Spring Framework Integration

2.原理:生成代理对象,通过反射的方式实现的对新对象每个字段进行复制

3.支持对象的复制,以及列表对象的复制:mapAtoB, mapList<A>ToList<B>

maven 依赖:

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

  

<!-- 与Spring进行整合的依赖 -->
<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer-spring</artifactId>
    <version>5.5.1</version>
</dependency>

工具类封装:

public class DozerCloneUtil {


    /**
 * 单个对象属性拷贝
 *
 * @param source 源对象
 * @param clazz 目标对象Class
 * @param <T> 目标对象类型
 * @param <M> 源对象类型
 * @return 目标对象
 */
 public static <T, M> T copyProperties(M source, Class<T> clazz) {
 if (source == null || clazz == null) {
            throw new IllegalArgumentException();
        }
        Mapper mapper = BeanHolder.MAPPER.getMapper();
        return mapper.map(source, clazz);
    }

    /**
 * 列表对象拷贝
 *
 * @param sources 源列表
 * @param clazz 源列表对象Class
 * @param <T> 目标列表对象类型
 * @param <M> 源列表对象类型
 * @return 目标列表
 */
 public static <T, M> List<T> copyObjects(List<M> sources, Class<T> clazz) {
        if (sources == null || clazz == null) {
            throw new IllegalArgumentException();
        }
 return Optional.of(sources)
                .orElse(new ArrayList<>())
                .stream().map(m -> copyProperties(m, clazz))
                .collect(Collectors.toList());
    }

    /**
 * 单例
 * DozerBeanMapper使用单例,有利于提高程序性能
 */
 private enum BeanHolder {
 MAPPER;

 private DozerBeanMapper mapper;

 BeanHolder() {
 this.mapper = new DozerBeanMapper();
 }

 public DozerBeanMapper getMapper() {
 return mapper;
 }
 }

}

      4.对于两个对象中不同的字段可以通过xml的进行配置,该方式推荐与Spring进行整合

1.在src/resources目录下创建映射文件dozer-mapping.xml

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xsi:schemaLocation="http://dozer.sourceforge.net http://dozer.sourceforge.net/schema/beanmapping.xsd">
  
 <!-- <class-a>指定所要复制的源对象,<class-b>复制的目标对象,<a>源对象的属性名, <b>目标对象的属性名。
 wildcard默认为true,在此时默认对所有属性进行map,如果为false,则只对在xml文件中配置的属性进行map。 -->
	<configuration>
		<stop-on-errors>false</stop-on-errors>
		<date-format>MM/dd/yyyy HH:mm</date-format>
		<wildcard>true</wildcard>
	</configuration>
 <mapping >
     <class-a>目标对象</class-a>
     <class-b>源对象</class-b>
   
配置同类型不同名的字段
 <field>
      <a>name</a>
      <b>value</b>
    </field>
  </mapping>
</mappings>

2.与Spring进行整合,在在src/resources目录下创建映射文件spring-dozer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"
 default-autowire="byName" default-lazy-init="false">

    <bean id="mapper" class="org.dozer.spring.DozerBeanMapperFactoryBean">
        <property name="mappingFiles">
            <list>
                <value>classpath*:dozer-conig/dozerBeanMapper.xml</value>
            </list>
        </property>
    </bean>
</beans>

3.使用方式

    @Autowired
    Mapper mapper;

  //复制对象

  XXX xxx = mapper.map(src, dec.class);

6.利用Orika进行复制对象

原理: Orika使用字节码生成器创建开销最小的快速映射,比其他基于反射方式实现(如,Dozer)更快。也支持自定义映射字段

性能:大概是Dozer的8-10 倍

maven依赖:

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.5.4</version>
</dependency>

工具类:

public enum OrikarMapperUtil{

    /**
 * 实例
 */
 INSTANCE;

    /**
 * 默认字段工厂
 */
 private static final MapperFactory MAPPER_FACTORY = new DefaultMapperFactory.Builder().build();

    /**
 * 默认字段实例
 */
 private static final MapperFacade MAPPER_FACADE = MAPPER_FACTORY.getMapperFacade();

    /**
 * 默认字段实例集合
 */
 private static Map<String, MapperFacade> CACHE_MAPPER_FACADE_MAP = new ConcurrentHashMap<>();

    /**
 * 映射实体(默认字段)
 *
 * @param toClass 映射类对象2
 * @param data 数据(对象)
 * @return 映射类对象
 */
 public static <E, T> E map(Class<E> toClass, T data) {
        return MAPPER_FACADE.map(data, toClass);
    }

    /**
 * 映射实体(自定义配置)
 *
 * @param toClass 映射类对象
 * @param data 数据(对象)
 * @param configMap 自定义配置
 * @return 映射类对象
 */
 public <E, T> E map(Class<E> toClass, T data, Map<String, String> configMap) {
        MapperFacade mapperFacade = this.getMapperFacade(toClass, data.getClass(), configMap);
        return mapperFacade.map(data, toClass);
    }

    /**
 * 映射集合(默认字段)
 *
 * @param toClass 映射类对象
 * @param data 数据(集合)
 * @return 映射类对象
 */
 public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data) {
        return MAPPER_FACADE.mapAsList(data, toClass);
    }


    /**
 * 映射集合(自定义配置)
 *
 * @param toClass 映射类
 * @param data 数据(集合)
 * @param configMap 自定义配置
 * @return 映射类对象
 */
 public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data, Map<String, String> configMap) {
        T t = data.stream().findFirst().orElseThrow(() -> new RuntimeException("映射集合,数据集合为空"));
        MapperFacade mapperFacade = this.getMapperFacade(toClass, t.getClass(), configMap);
        return mapperFacade.mapAsList(data, toClass);
    }

    /**
 * 获取自定义映射
 *
 * @param toClass 映射类
 * @param dataClass 数据映射类
 * @param configMap 自定义配置
 * @return 映射类对象
 */
 private <E, T> MapperFacade getMapperFacade(Class<E> toClass, Class<T> dataClass, Map<String, String> configMap) {
        String mapKey = dataClass.getCanonicalName() + "_" + toClass.getCanonicalName();
        MapperFacade mapperFacade = CACHE_MAPPER_FACADE_MAP.get(mapKey);
        if (Objects.isNull(mapperFacade)) {
            MapperFactory factory = new DefaultMapperFactory.Builder().build();
            ClassMapBuilder classMapBuilder = factory.classMap(dataClass, toClass);
            configMap.forEach(classMapBuilder::field);
            classMapBuilder.byDefault().register();
            mapperFacade = factory.getMapperFacade();
            CACHE_MAPPER_FACADE_MAP.put(mapKey, mapperFacade);
        }
        return mapperFacade;
    }


}
 

7.使用cloning实现深度拷贝

cloning:该库信息比较少,是一个较为冷门的克隆库,唯一的信息是maven依赖库中的简介

 

克隆库是一个小型的开放源码Java库(Apache许可),它对对象进行深度克隆。对象不必实现可克隆接口。实际上,这个库可以克隆任何Java对象。它可以在缓存实现中使用,如果你不希望缓存对象被修改,或者当你想要创建一个对象的深度副本时。

maven依赖

<dependency>
    <groupId>uk.com.robust-it</groupId>
    <artifactId>cloning</artifactId>
    <version>1.9.12</version>
</dependency>

工具类

public class CloningUtil {
    private static final Cloner cloner = new Cloner();

    /**
 * 复制对象(深度拷贝)
 *
 * @param sourceObject
 * @param <T>
 * @return
 */

 public static <T> T clone(final T sourceObject) {
        if (sourceObject == null) {
            return null;
        }
        return cloner.deepClone(sourceObject);
    }


}

3.2 浅拷贝的常见做法

1.Spring的BeanUtils,2.Commons-BeanUtils,3MapStruct,4.BeanCopier

重点介绍 BeanCopier和MapStruct

MapStruct使用

  1. MapStruct介绍

    MapStruct是用于生成类型安全的bean映射类的Java注解处理器。你所要做的就是定义一个映射器接口,声明任何需要映射的方法。在编译过程中,MapStruct将生成该接口的实现。此实现使用纯Java的方法调用源对象和目标对象之间进行映射,并非Java反射机制。

  2. MapStruct原理
    在代码编译阶段生成对应的赋值代码,底层原理还是调用getter/setter方法,但是这是由工具替我们完成,MapStruct在不影响性能的情况下,解决了手工赋值方式弊端。
  3. 两种使用方式

    Maven依赖

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.3.1.Final</version>
    </dependency>
    
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.3.1.Final</version>
    </dependency>


    3.1静态方法调用

    @Mapper
    public interface ConvertMapper {
    
        ConvertMapper INSTANCE = Mappers.getMapper(ConvertMapper.class);
    
    
     // 通过在Mappings注解里配置多个mapping,实现字段不一致的情况的映射
     @Mappings(
        @Mapping(target = "",source = "")
        )
        MappingTaskCacheDto mapToTaskDto(MappingTaskCacheDto mappingTaskCacheDto);
    
    }

    调用:

    MappingTaskCacheDto mappingTaskCacheDto = BeanFactory.createMappingTaskCacheDto();
    MappingTaskCacheDto mappingTaskCacheResult = ConvertMapper.INSTANCE.mapToTaskDto(mappingTaskCacheDto);

    3.2 接口注入

    @Mapper(componentModel = "spring")  //与Spring进行整合
    public interface MapperStructWithSpring {
     // 通过在Mappings注解里配置多个mapping,实现字段不一致的情况的映射
     @Mappings(
        @Mapping(target = "",source = "")
        )
        MappingTaskCacheDto mapToTaskDto(MappingTaskCacheDto mappingTaskCacheDto);
    
    }

    使用:

    @Autowired
    private MapperStructWithSpring mapperStructWithSpring;
     
    MappingTaskCacheDto mappingTaskCacheResult mapperStructWithSpring.mapToTaskDto(mappingTaskCacheDto);

    BeanCopier的使用1.Maven依赖 Spring自带了

    2.使用方式:

    BeanCopier beanCopier = BeanCopier.create(MappingTaskCacheDto.class, MappingTaskCacheDto.class, false);
    //创建源对象并赋值数据
    MappingTaskCacheDto mappingTaskCacheDto = createMappingTaskCacheDto();
    
    //创建目标对象
    MappingTaskCacheDto mappingTaskCacheResult = new MappingTaskCacheDto();
    
    beanCopier.copy(mappingTaskCacheDto,mappingTaskCacheResult,null);

4.方法的性能对比

4.1 基本参数:

    JVM参数:  -Xms512m    -Xmx1024m

4.2 性能测试:针对同一个对象分别测试复制 一万,十万,百万个对象时所需的时间。

深拷贝

一万十万一百万
Orika398ms859ms4941ms
FastJson391ms1888ms16412ms
Cloning372ms3267ms32396ms
KryoCloing226ms1166ms10295ms
Dozer2767ms25237ms

 

 

5.相关Demo

等待中

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值