☰
如何高效进行对象拷贝?浅拷贝与深拷贝的陷阱,你知道吗?
2026/10/14 1:56:50 网站建设 项目流程

全文目录:

    • 开篇语
    • 前言
    • 一、浅拷贝与深拷贝:它们的区别与陷阱
      • 1. 什么是浅拷贝?
      • 2. 什么是深拷贝?
    • 二、对象属性复制的正确方式:Spring BeanUtils、Apache BeanUtils 与 MapStruct
      • 1. 使用 Spring 的 BeanUtils
      • 2. 使用 Apache 的 BeanUtils
      • 3. 使用 MapStruct
      • 4. 性能比较:BeanUtils vs 手写 vs MapStruct
    • 三、实战场景:DTO 与 Entity 之间的转换
    • 四、总结
    • 文末

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

我们在日常开发中,常常需要在对象之间进行数据传递,尤其是在不同层之间的对象转换。例如,在数据层使用实体类(Entity),在服务层或传输层使用数据传输对象(DTO)。每次我们将一个对象的属性复制到另一个对象时,都面临着一个重要的决定:应该使用浅拷贝还是深拷贝?不同的拷贝方式会直接影响我们的程序运行和数据的独立性。

对于许多开发者来说,浅拷贝和深拷贝之间的差异可能并不十分明显,但它们之间的微妙区别却决定了数据是否会“共享”或“隔离”。本篇文章,我们将通过对浅拷贝与深拷贝的讨论,以及 Spring 和 Apache 提供的BeanUtils和更现代化的MapStruct的使用,深入探讨如何在实际开发中高效地进行对象属性复制。我们还将对比不同方法的性能,帮助你在开发中做出合适的选择。

一、浅拷贝与深拷贝:它们的区别与陷阱

1. 什么是浅拷贝?

浅拷贝的核心思想是:如果对象中包含基本类型的属性,它们会被直接复制;如果对象中包含引用类型的属性,那么这些引用类型的属性只会复制引用(即内存地址),而不会复制引用对象本身。这就意味着,原始对象和拷贝对象中引用类型的属性会指向同一块内存空间。

我们通过一个例子来看看浅拷贝的实际情况:

publicclassPerson{privateStringname;privateAddressaddress;// Getters and Setters}publicclassAddress{privateStringcity;privateStringstreet;// Getters and Setters}
/** * @Author ms * @Date 2025年6月22日20:43:49 */publicclassTest1{publicstaticvoidmain(String[]args){// 浅拷贝示例Personperson1=newPerson("John",newAddress("New York","5th Avenue"));Personperson2=person1;// 只是引用复制,指向相同的内存person2.getAddress().setCity("San Francisco");System.out.println(person1.getAddress().getCity());// 输出 "San Francisco"}}

在这个例子中,person1和person2是指向同一个Address对象的引用。因此,当我们修改person2中的address时,person1的address也会被修改。这就是浅拷贝的特性:引用类型字段共享同一块内存区域。

本地测试结果展示如下:

根据如上测试用例,我本地演示结果展示如下,仅供参考哈,你们也可以自行修改测试用例或者添加更多的测试数据或测试方法,进行熟练学习以此加深理解。

代码解析:

针对如上示例代码,这里我给大家详细的代码剖析下,以便于帮助大家理解的更为透彻,帮助大家早日掌握。如下这段代码案例我演示了 Java 中“浅拷贝”的概念,以下是详细解析:

🧠 程序核心思想:浅拷贝

这段代码案例展示了一个对象被赋值给另一个变量后,两者引用同一个内存地址的现象,因此修改一个对象的内部属性,也会影响另一个对象。

👤 示例对象结构:

程序中使用了两个类(虽然未明示,但根据代码逻辑可推断):

  • Person:拥有name和Address成员。
  • Address:拥有city和street等属性。

🧪 具体过程解释:

  1. 创建了一个Person对象person1,内部包含一个Address对象(城市是 New York)。
  2. 然后将person1的引用赋值给person2,这并不是创建了一个新对象,而是person2和person1指向了同一个 Person 对象。
  3. 修改person2的地址对象,将城市名改为 “San Francisco”。
  4. 最后打印person1的地址城市,结果是 “San Francisco”。

🔍 原因分析:

这是浅拷贝(shallow copy):

  • person2 = person1并没有复制person1的数据,而是复制了引用;
  • 所以person1.getAddress()和person2.getAddress()返回的是同一个 Address 实例;
  • 修改其中一个的地址,就影响了另一个。

✅ 总结:

这段代码的目的是说明:Java 中对象赋值是引用复制,而不是值复制。要真正创建一个不受原对象影响的副本,需要使用深拷贝(deep copy)策略,比如:

  • 手动复制每一层对象;
  • 使用序列化或第三方工具类(如 Apache Commons Lang 的SerializationUtils.clone())。

这个浅拷贝示例在开发中非常常见,理解它对避免意外共享对象副作用非常关键。

2. 什么是深拷贝?

与浅拷贝不同,深拷贝会创建一个新对象,并递归地复制原始对象及其所有引用类型的字段,确保新对象和原对象之间完全独立。换句话说,深拷贝不仅复制对象本身,还会复制对象所引用的所有其他对象。因此,修改新对象中的字段不会影响原对象。

我们通过一个深拷贝的例子来说明:

publicclassPersonimplementsCloneable{privateStringname;privateAddressaddress;// Getter and Setter@OverridepublicObjectclone()throwsCloneNotSupportedException{Personperson=(Person)super.clone();person.setAddress((Address)person.getAddress().clone());returnperson;}}// 深拷贝示例Personperson1=newPerson("John",newAddress("New York","5th Avenue"));Personperson2=(Person)person1.clone();// 深拷贝,两个对象完全独立person2.getAddress().setCity("San Francisco");System.out.println(person1.getAddress().getCity());// 输出 "New York"

在这个例子中,person1和person2是完全独立的对象,修改person2的address字段不会影响person1。这是深拷贝的特性,确保了对象的完全复制。

二、对象属性复制的正确方式:Spring BeanUtils、Apache BeanUtils 与 MapStruct

在 Java 中,如果我们想要复制一个对象的属性到另一个对象,可以通过手写代码、使用 Spring 或 Apache 提供的BeanUtils,或者采用更现代化的MapStruct来实现。接下来,我们会对这些方法进行详细分析,看看它们的优缺点以及适用场景。

1. 使用 Spring 的 BeanUtils

Spring 的BeanUtils是最常用的属性复制工具之一。BeanUtils.copyProperties()方法能够将源对象的属性值复制到目标对象中,适用于简单的对象属性复制场景。代码实现如下:

importorg.springframework.beans.BeanUtils;publicclassExample{publicstaticvoidmain(String[]args){SourceBeansource=newSourceBean("John",25);TargetBeantarget=newTargetBean();BeanUtils.copyProperties(source,target);System.out.println(target.getName());// 输出 "John"System.out.println(target.getAge());// 输出 25}}classSourceBean{privateStringname;privateintage;// Getters and Setters}classTargetBean{privateStringname;privateintage;// Getters and Setters}

优点:

  • 简单易用,不需要写复杂的代码。
  • 可以处理基础类型和属性的复制。

缺点:

  • 性能较差:BeanUtils基于反射实现属性复制,效率低,尤其是对于大对象或频繁调用时。
  • 不支持深拷贝:它仅支持浅拷贝,对于包含引用类型字段的对象,它不会进行深拷贝。

2. 使用 Apache 的 BeanUtils

Apache 的BeanUtils和 Spring 提供的BeanUtils类似,也是基于反射的属性复制工具。使用方法和 Spring 基本相同,代码示例如下:

importorg.apache.commons.beanutils.BeanUtils;publicclassExample{publicstaticvoidmain(String[]args)throwsException{SourceBeansource=newSourceBean("John",25);TargetBeantarget=newTargetBean();BeanUtils.copyProperties(target,source);System.out.println(target.getName());// 输出 "John"System.out.println(target.getAge());// 输出 25}}classSourceBean{privateStringname;privateintage;// Getters and Setters}classTargetBean{privateStringname;privateintage;// Getters and Setters}

优缺点与 Spring 的BeanUtils类似,主要区别在于它是 Apache Commons 提供的工具类,且性能问题同样存在。

3. 使用 MapStruct

MapStruct 是近年来流行的一种代码生成工具,它通过注解生成高效的属性复制代码,避免了反射的性能开销。与 Spring 或 Apache 的BeanUtils不同,MapStruct 在编译时生成映射代码,使用时无需反射,因此性能非常高。

importorg.mapstruct.Mapper;importorg.mapstruct.factory.Mappers;@MapperpublicinterfacePersonMapper{PersonMapperINSTANCE=Mappers.getMapper(PersonMapper.class);TargetBeansourceToTarget(SourceBeansource);}classSourceBean{privateStringname;privateintage;// Getters and Setters}classTargetBean{privateStringname;privateintage;// Getters and Setters}

MapStruct 会在编译时生成具体的转换代码,而不是在运行时反射,这使得它的性能要比BeanUtils高得多,尤其是在复杂对象的转换上。

优点:

  • 高性能:通过编译时生成代码,避免了反射带来的性能开销。
  • 支持深拷贝:MapStruct 能够处理更复杂的类型转换,包括深拷贝和多层嵌套对象的转换。

缺点:

  • 学习成本较高:需要了解注解和接口的使用,以及如何配置 MapStruct。
  • 配置稍微复杂:需要依赖构建工具来生成代码,如 Maven 或 Gradle。

4. 性能比较:BeanUtils vs 手写 vs MapStruct

在性能上,MapStruct的优势非常明显,它通过在编译时生成代码,避免了反射的性能瓶颈。BeanUtils和Apache BeanUtils都使用了反射机制,性能较差,尤其是在需要频繁进行属性复制的场景中,可能会成为瓶颈。

我们可以通过以下流程图直观地了解这三者之间的差异:

性能最优

性能较好

性能较差

Apache BeanUtils

三、实战场景:DTO 与 Entity 之间的转换

在企业级开发中,DTO 和 Entity 之间的转换是常见的需求。DTO(数据传输对象)通常用于前端数据交互,而 Entity(实体类)则代表数据库中的数据模型。我们需要将数据库查询得到的实体类转换成 DTO,以便传输给前端或进行进一步处理。

使用BeanUtils进行 DTO 与 Entity 之间的转换非常简单,但在复杂的数据结构转换场景中,推荐使用MapStruct,它能高效、准确地完成深拷贝,确保对象之间完全独立。

@MapperpublicinterfaceUserMapper{UserMapperINSTANCE=Mappers.getMapper(UserMapper.class);UserDTOentityToDto(UserEntityentity);UserEntitydtoToEntity(UserDTOdto);}classUserEntity{privateStringname;privateStringemail;// Getters and Setters}classUserDTO{privateStringname;privateStringemail;// Getters and Setters}

通过 MapStruct,我们可以快速生成 DTO 和 Entity 之间的转换代码,避免手动编写大量冗余的转换代码。

代码解析:

针对如上示例代码,这里我给大家详细的代码剖析下,以便于帮助大家理解的更为透彻,帮助大家早日掌握。

如上这段代码展示了如何使用MapStruct框架在 Java 中进行对象映射(Object Mapping),也就是将一个类的对象转换为另一个类的对象。以下是详细代码解析:

🧩背景:什么是对象映射?

在实际开发中,常常会有不同层的对象模型,例如:

  • Entity:用于数据库持久化(JPA/Hibernate);
  • DTO(Data Transfer Object):用于数据传输,特别是在前后端交互或服务调用中。

虽然这些对象字段可能相同或相似,但通常不能直接互用,因此需要手动写转换逻辑,这会导致大量重复代码。

🧠 MapStruct 的作用:

MapStruct是一个编译期注解处理器,它可以自动为我们生成这些对象转换的代码:

  • 快速;
  • 类型安全;
  • 不用手写 getter/setter 映射逻辑。

🔧代码详解:

  1. 接口定义(UserMapper)
@MapperpublicinterfaceUserMapper{UserMapperINSTANCE=Mappers.getMapper(UserMapper.class);
  • @Mapper:标记这是一个 MapStruct 映射接口;
  • INSTANCE:这是获取自动生成的 Mapper 实例的标准做法;
  • Mappers.getMapper(...):MapStruct 在编译期会生成实现类。
  1. 映射方法
UserDTOentityToDto(UserEntityentity);UserEntitydtoToEntity(UserDTOdto);
  • 这两个方法定义了从Entity到DTO,以及反向的转换;
  • MapStruct 会根据字段名称和类型相同的原则,自动完成映射逻辑。

👤 类定义(UserEntity和UserDTO)

这两个类都有两个相同的字段:name和email,并包含 getter 和 setter。

由于字段名一致,MapStruct 可以自动完成对应字段的映射,无需额外配置。

✅ 总结:

这段代码通过使用 MapStruct:

  • 避免了手动编写繁琐的字段赋值;
  • 保持了 DTO 和 Entity 的独立性;
  • 让数据层与表示层之间的转换变得高效、规范;
  • 是企业级应用开发中常用的模型转换方案。

要使这段代码实际运行,还需添加相关依赖(如在 Maven 中引入mapstruct和mapstruct-processor),并在构建时启用注解处理。

四、总结

通过本文的讲解,我们深入了解了对象拷贝中浅拷贝和深拷贝的区别,以及它们可能带来的陷阱。同时,探讨了如何利用工具类(如 Spring 的BeanUtils、Apache 的BeanUtils和MapStruct)高效地进行对象属性复制。对于大多数简单场景,BeanUtils已经足够满足需求,但在处理更复杂的对象转换时,尤其是需要深拷贝的情况,MapStruct绝对是一个更高效的选择。

在实际开发中,选择合适的工具将大大提升开发效率和代码质量。希望通过本文的讨论,你能更加清楚地了解对象拷贝的正确方式,避免陷入浅拷贝与深拷贝的陷阱,编写出高效、可维护的代码。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询