开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
我们在日常开发中,常常需要在对象之间进行数据传递,尤其是在不同层之间的对象转换。例如,在数据层使用实体类(Entity),在服务层或传输层使用数据传输对象(DTO)。每次我们将一个对象的属性复制到另一个对象时,都面临着一个重要的决定:应该使用浅拷贝还是深拷贝?不同的拷贝方式会直接影响我们的程序运行和数据的独立性。
对于许多开发者来说,浅拷贝和深拷贝之间的差异可能并不十分明显,但它们之间的微妙区别却决定了数据是否会"共享"或"隔离"。本篇文章,我们将通过对浅拷贝与深拷贝的讨论,以及 Spring 和 Apache 提供的 BeanUtils
和更现代化的 MapStruct
的使用,深入探讨如何在实际开发中高效地进行对象属性复制。我们还将对比不同方法的性能,帮助你在开发中做出合适的选择。
一、浅拷贝与深拷贝:它们的区别与陷阱
1. 什么是浅拷贝?
浅拷贝的核心思想是:如果对象中包含基本类型的属性,它们会被直接复制;如果对象中包含引用类型的属性,那么这些引用类型的属性只会复制引用(即内存地址),而不会复制引用对象本身。这就意味着,原始对象和拷贝对象中引用类型的属性会指向同一块内存空间。
我们通过一个例子来看看浅拷贝的实际情况:
java
public class Person {
private String name;
private Address address;
// Getters and Setters
}
public class Address {
private String city;
private String street;
// Getters and Setters
}
java
/**
* @Author ms
* @Date 2025年6月22日20:43:49
*/
public class Test1 {
public static void main(String[] args) {
// 浅拷贝示例
Person person1 = new Person("John", new Address("New York", "5th Avenue"));
Person person2 = 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
等属性。
🧪 具体过程解释:
- 创建了一个
Person
对象person1
,内部包含一个Address
对象(城市是 New York)。 - 然后将
person1
的引用赋值给person2
,这并不是创建了一个新对象,而是person2
和person1
指向了同一个 Person 对象。 - 修改
person2
的地址对象,将城市名改为 "San Francisco"。 - 最后打印
person1
的地址城市,结果是 "San Francisco"。
🔍 原因分析:
这是浅拷贝(shallow copy):
person2 = person1
并没有复制person1
的数据,而是复制了引用;- 所以
person1.getAddress()
和person2.getAddress()
返回的是同一个 Address 实例; - 修改其中一个的地址,就影响了另一个。
✅ 总结:
这段代码的目的是说明:Java 中对象赋值是引用复制,而不是值复制。要真正创建一个不受原对象影响的副本,需要使用深拷贝(deep copy)策略,比如:
- 手动复制每一层对象;
- 使用序列化或第三方工具类(如 Apache Commons Lang 的
SerializationUtils.clone()
)。
这个浅拷贝示例在开发中非常常见,理解它对避免意外共享对象副作用非常关键。
2. 什么是深拷贝?
与浅拷贝不同,深拷贝会创建一个新对象,并递归地复制原始对象及其所有引用类型的字段,确保新对象和原对象之间完全独立。换句话说,深拷贝不仅复制对象本身,还会复制对象所引用的所有其他对象。因此,修改新对象中的字段不会影响原对象。
我们通过一个深拷贝的例子来说明:
java
public class Person implements Cloneable {
private String name;
private Address address;
// Getter and Setter
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.setAddress((Address) person.getAddress().clone());
return person;
}
}
// 深拷贝示例
Person person1 = new Person("John", new Address("New York", "5th Avenue"));
Person person2 = (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()
方法能够将源对象的属性值复制到目标对象中,适用于简单的对象属性复制场景。代码实现如下:
java
import org.springframework.beans.BeanUtils;
public class Example {
public static void main(String[] args) {
SourceBean source = new SourceBean("John", 25);
TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);
System.out.println(target.getName()); // 输出 "John"
System.out.println(target.getAge()); // 输出 25
}
}
class SourceBean {
private String name;
private int age;
// Getters and Setters
}
class TargetBean {
private String name;
private int age;
// Getters and Setters
}
优点:
- 简单易用,不需要写复杂的代码。
- 可以处理基础类型和属性的复制。
缺点:
- 性能较差 :
BeanUtils
基于反射实现属性复制,效率低,尤其是对于大对象或频繁调用时。 - 不支持深拷贝:它仅支持浅拷贝,对于包含引用类型字段的对象,它不会进行深拷贝。
2. 使用 Apache 的 BeanUtils
Apache 的 BeanUtils
和 Spring 提供的 BeanUtils
类似,也是基于反射的属性复制工具。使用方法和 Spring 基本相同,代码示例如下:
java
import org.apache.commons.beanutils.BeanUtils;
public class Example {
public static void main(String[] args) throws Exception {
SourceBean source = new SourceBean("John", 25);
TargetBean target = new TargetBean();
BeanUtils.copyProperties(target, source);
System.out.println(target.getName()); // 输出 "John"
System.out.println(target.getAge()); // 输出 25
}
}
class SourceBean {
private String name;
private int age;
// Getters and Setters
}
class TargetBean {
private String name;
private int age;
// Getters and Setters
}
优缺点与 Spring 的 BeanUtils
类似,主要区别在于它是 Apache Commons 提供的工具类,且性能问题同样存在。
3. 使用 MapStruct
MapStruct 是近年来流行的一种代码生成工具,它通过注解生成高效的属性复制代码,避免了反射的性能开销。与 Spring 或 Apache 的 BeanUtils
不同,MapStruct 在编译时生成映射代码,使用时无需反射,因此性能非常高。
java
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
TargetBean sourceToTarget(SourceBean source);
}
class SourceBean {
private String name;
private int age;
// Getters and Setters
}
class TargetBean {
private String name;
private int age;
// Getters and Setters
}
MapStruct 会在编译时生成具体的转换代码,而不是在运行时反射,这使得它的性能要比 BeanUtils
高得多,尤其是在复杂对象的转换上。
优点:
- 高性能:通过编译时生成代码,避免了反射带来的性能开销。
- 支持深拷贝:MapStruct 能够处理更复杂的类型转换,包括深拷贝和多层嵌套对象的转换。
缺点:
- 学习成本较高:需要了解注解和接口的使用,以及如何配置 MapStruct。
- 配置稍微复杂:需要依赖构建工具来生成代码,如 Maven 或 Gradle。
4. 性能比较:BeanUtils vs 手写 vs MapStruct
在性能上,MapStruct
的优势非常明显,它通过在编译时生成代码,避免了反射的性能瓶颈。BeanUtils
和 Apache BeanUtils
都使用了反射机制,性能较差,尤其是在需要频繁进行属性复制的场景中,可能会成为瓶颈。
我们可以通过以下流程图直观地了解这三者之间的差异:
三、实战场景:DTO 与 Entity 之间的转换
在企业级开发中,DTO 和 Entity 之间的转换是常见的需求。DTO(数据传输对象)通常用于前端数据交互,而 Entity(实体类)则代表数据库中的数据模型。我们需要将数据库查询得到的实体类转换成 DTO,以便传输给前端或进行进一步处理。
使用 BeanUtils
进行 DTO 与 Entity 之间的转换非常简单,但在复杂的数据结构转换场景中,推荐使用 MapStruct
,它能高效、准确地完成深拷贝,确保对象之间完全独立。
java
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO entityToDto(UserEntity entity);
UserEntity dtoToEntity(UserDTO dto);
}
class UserEntity {
private String name;
private String email;
// Getters and Setters
}
class UserDTO {
private String name;
private String email;
// Getters and Setters
}
通过 MapStruct,我们可以快速生成 DTO 和 Entity 之间的转换代码,避免手动编写大量冗余的转换代码。
代码解析:
针对如上示例代码,这里我给大家详细的代码剖析下,以便于帮助大家理解的更为透彻,帮助大家早日掌握。
如上这段代码展示了如何使用 MapStruct 框架在 Java 中进行对象映射(Object Mapping),也就是将一个类的对象转换为另一个类的对象。以下是详细代码解析:
🧩 背景:什么是对象映射?
在实际开发中,常常会有不同层的对象模型,例如:
Entity
:用于数据库持久化(JPA/Hibernate);DTO
(Data Transfer Object):用于数据传输,特别是在前后端交互或服务调用中。
虽然这些对象字段可能相同或相似,但通常不能直接互用,因此需要手动写转换逻辑,这会导致大量重复代码。
🧠 MapStruct 的作用:
MapStruct 是一个编译期注解处理器,它可以自动为我们生成这些对象转换的代码:
- 快速;
- 类型安全;
- 不用手写 getter/setter 映射逻辑。
🔧 代码详解:
- 接口定义(
UserMapper
)
java
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapper
:标记这是一个 MapStruct 映射接口;INSTANCE
:这是获取自动生成的 Mapper 实例的标准做法;Mappers.getMapper(...)
:MapStruct 在编译期会生成实现类。
- 映射方法
java
UserDTO entityToDto(UserEntity entity);
UserEntity dtoToEntity(UserDTO dto);
- 这两个方法定义了从
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 !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。 ⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!