【深拷贝和浅拷贝】

本文详细介绍了Java中深拷贝和浅拷贝的概念,提供了多种实现深拷贝的方法,包括cloneable接口、序列化、Json转换、Dozer和Orika库等,并对比了它们的性能。同时,文章还讨论了浅拷贝的实现,如Spring的BeanUtils,并指出在选择拷贝方式时应考虑的性能和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深拷贝和浅拷贝

在开发过程中,经常会用到对象复制,在复制后如果再对对象进行修改,可能会引入一些问题。如果在开发过程中,使用了浅拷贝创建新的对象并对对象中的引用类型变量进行了修改,那么这将直接导致原对象也修改了,从而导致一些问题。

首先看下基本概念

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值