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
|
---|
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.使用方式
//复制对象 |
---|
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使用
- MapStruct介绍
MapStruct是用于生成类型安全的bean映射类的Java注解处理器。你所要做的就是定义一个映射器接口,声明任何需要映射的方法。在编译过程中,MapStruct将生成该接口的实现。此实现使用纯Java的方法调用源对象和目标对象之间进行映射,并非Java反射机制。
- MapStruct原理
在代码编译阶段生成对应的赋值代码,底层原理还是调用getter/setter方法,但是这是由工具替我们完成,MapStruct在不影响性能的情况下,解决了手工赋值方式弊端。 -
两种使用方式
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 性能测试:针对同一个对象分别测试复制 一万,十万,百万个对象时所需的时间。
深拷贝 | 一万 | 十万 | 一百万 |
Orika | 398ms | 859ms | 4941ms |
FastJson | 391ms | 1888ms | 16412ms |
Cloning | 372ms | 3267ms | 32396ms |
KryoCloing | 226ms | 1166ms | 10295ms |
Dozer | 2767ms | 25237ms | 无 |
5.相关Demo
等待中