如何高效进行对象拷贝?浅拷贝与深拷贝的陷阱,你知道吗?

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/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"
    }
}

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

本地测试结果展示如下:

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

代码解析:

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

🧠 程序核心思想:浅拷贝

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

👤 示例对象结构:

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

  • Person:拥有 nameAddress 成员。
  • Address:拥有 citystreet 等属性。

🧪 具体过程解释:

  1. 创建了一个 Person 对象 person1,内部包含一个 Address 对象(城市是 New York)。
  2. 然后将 person1 的引用赋值给 person2,这并不是创建了一个新对象,而是 person2person1 指向了同一个 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. 什么是深拷贝?

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

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

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"

在这个例子中,person1person2 是完全独立的对象,修改 person2address 字段不会影响 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 的优势非常明显,它通过在编译时生成代码,避免了反射的性能瓶颈。BeanUtilsApache BeanUtils 都使用了反射机制,性能较差,尤其是在需要频繁进行属性复制的场景中,可能会成为瓶颈。

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

graph LR A[手写代码] --> B[MapStruct] B --> C[BeanUtils] C --> D[Apache BeanUtils] A[性能最优] --> B[性能较好] --> C[性能较差]

三、实战场景: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 映射逻辑。

🔧 代码详解:

  1. 接口定义(UserMapper
java 复制代码
@Mapper
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
  • @Mapper:标记这是一个 MapStruct 映射接口;
  • INSTANCE:这是获取自动生成的 Mapper 实例的标准做法;
  • Mappers.getMapper(...):MapStruct 在编译期会生成实现类。
  1. 映射方法
java 复制代码
UserDTO entityToDto(UserEntity entity);
UserEntity dtoToEntity(UserDTO dto);
  • 这两个方法定义了从 EntityDTO,以及反向的转换;
  • MapStruct 会根据字段名称和类型相同的原则,自动完成映射逻辑。

👤 类定义(UserEntityUserDTO

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

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

✅ 总结:

这段代码通过使用 MapStruct:

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

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

四、总结

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

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

... ...

文末

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

... ...

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

wished for you successed !!!


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

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


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

相关推荐
鼠鼠我捏,要死了捏1 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw2 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友2 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls2 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh2 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫3 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong3 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊4 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉4 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment4 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源