深拷贝和浅拷贝
深拷贝和浅拷贝
在开发过程中,经常会用到对象复制,在复制后如果再对对象进行修改,可能会引入一些问题。如果在开发过程中,使用了浅拷贝创建新的对象并对对象中的引用类型变量进行了修改,那么这将直接导致原对象也修改了,从而导致一些问题。
首先看下基本概念
1、相关概念介绍
浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
目前常用的属性拷贝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct都是浅拷贝。
浅拷贝会创建一个新对象,新对象和原对象本身没有任何关系,新对象和原对象不等,但是新对象的属性和老对象相同。具体可以看如下区别:
-
如果属性是基本类型(int,double,long,boolean等),拷贝的就是基本类型的值;
-
如果属性是引用类型,拷贝的就是内存地址(即复制引用但不复制引用的对象) ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。这个方式称为深拷贝。
在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量。
深拷贝常见有以下四种实现方式:
构造函数
Serializable序列化
实现Cloneable接口
JSON序列化
也就是说浅拷贝只复制一个对象,传递引用,不能复制实例。而深拷贝对对象内部的引用均复制,它是创建一个新的实例,并且复制实例。
对于浅拷贝当对象的成员变量是基本数据类型时,两个对象的成员变量已有存储空间,赋值运算传递值,所以浅拷贝能够复制实例。但是当对象的成员变量是引用数据类型时,就不能实现对象的复制了。
2、深拷贝
2.1、实现JAVA自带的cloneable接口,重写clone方法进行实现(不推荐)
注意点:
① 如果有一个非原生成员,如自定义对象的成员,那么就需要:
该成员实现Cloneable接口并覆盖clone()方法,并将类的权限提升为public。
同时,修改被复制类的clone()方法,增加成员的克隆逻辑。
② 如果被复制对象不是直接继承Object,中间还有其它继承层次,每一层super类都需要实现Cloneable接口并覆盖clone()方法。与对象成员不同,继承关系中的clone不需要被复制类的clone()做多余的工作。
缺点:对于有大量vo,po的工程,这样做无疑增加了开发量。
2.2、手动New对象的方法(不推荐)
原理:人工构建对象,如果需要复制的对象中包含非基本类型,如List,对象等结构时,可以在需要的时候手动new对象,将属性值挨个调用set方法,比较繁琐,但无疑是最高效的做法
2.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;
}
}
2.4、利用Json转换法
原理:可以先将对象转化为JSON,再序列化为对象,和第一种方法类似。Json转换工具可以用Jackson或者Json-lib
代表:JSON.parseObject(JSON.toJSONString(xxx), xxx.class);
2.5、利用Dozer实现深拷贝
1.Dozer简介: Dozer 是一个对象转换工具。
Dozer可以在JavaBean到JavaBean之间进行递归数据复制,并且这些JavaBean可以是不同的复杂的类型。
所有的mapping,Dozer将会很直接的将名称相同的fields进行复制,如果field名不同,或者有特别的对应要求,则可以在xml中进行定义。也
可以与Spring进行整合:http://dozer.sourceforge.net/documentation/springintegration.html
2.原理:生成代理对象,通过反射的方式实现的对新对象每个字段进行复制
3.支持对象的复制,以及列表对象的复制:mapAtoB, mapListToList
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);
2.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;
}
}
2.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、浅拷贝
3.1、Spring的BeanUtils
【强制】避免用 Apache Beanutils 进行属性的 copy。(阿里巴巴Java开发手册)
Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier,注意 均是浅拷贝。
3.2、Commons-BeanUtils
3.3、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>
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);
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);
3.4、BeanCopier
BeanCopier的使用
Maven依赖 Spring自带了
使用方式:
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