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

开篇语

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


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

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


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

相关推荐
剁椒豆腐脑9 分钟前
阶段二JavaSE进阶阶段之设计模式&继承 2.2
java·设计模式·跳槽·学习方法·改行学it
扫地僧98525 分钟前
免费1000套编程教学视频资料视频(涉及Java、python、C C++、R语言、PHP C# HTML GO)
java·c++·音视频
青春:一叶知秋27 分钟前
【C++开发】CMake构建工具
java·开发语言·c++
顺丰同城前端技术团队38 分钟前
用大白话聊Deepseek MoE
前端·人工智能·后端
77tian38 分钟前
Java Collections工具类:高效集合操作
java·开发语言·windows·microsoft·list
2401_8260976241 分钟前
JavaEE-Mybatis初阶
java·java-ee·mybatis
KIDAKN44 分钟前
JavaEE->多线程2
java·算法·java-ee
啊哈灵机一动1 小时前
golang开发的一些注意事项(二·)
后端·面试
wu_android1 小时前
Java匿名内部类访问全局变量和局部变量的注意事项
java·开发语言
喵手1 小时前
领导让我同事封装一个自定义工具类?结果她说要三小时...
java·后端·java ee