Java对象深拷贝 终极方案 deep clone an object

本文深入探讨Java中实现对象深拷贝的最佳实践,详细对比并推荐使用Jackson库的三种序列化方法,避免传统工具类可能导致的问题。

定义 深拷贝

  • 必须是全新的对象,object堆内存地址是全新的;
  • 对象的各个属性值 prop Value,也是全新的,指向全新的堆内存地址。
  • 作用: 原有旧对象和新对象,可以独立修改各自的属性值,互相之间没影响。
  • 场景:
    • java的method 参数传递的是内存地址,也就是对象的引用句柄,只有 primitive 基本类型和String 作为参数时,传递的才是值
    • 某个对象,需要作为 多个方法的参数,进行不同的操作。
      为了防止这个原始对象的属性值被意外修改,就需要 深拷贝为新对象。
      操作新对象,不会对 原始对象有任何影响。
    • 该对象作为参数进行传递的次数越多,因为属性的内存地址都是一样的,属性值被修改的风险就越高

深拷贝常见误区

spring / apache commons 等工具类的 BeanUtils.copy 方法 ❌

  • 查看源码可知,使用的是 对象的get/set 方式实现的
  • 对象虽然是新的,但是 属性值的内存地址是相同的
  • 修改新对象的属性值,会同时影响原有对象的属性值
  • 如果属性是 特殊类型比如map 或者 list 或者 嵌套对象属性,可能就不好使了
    在这里插入图片描述

正确做法: 上中下3策 ✔

json 序列化 (用jackson,别用其他的gson/fastjson/json-lib 等,不解释)

类似 java 的对象序列化和反序列化过程( object Serialization & deserialization ),产生的是全新的对象
不同的是,java 对象序列化需要落地为磁盘文件,jackson 序列化则正常运行在jvm内存中。

Jackson 是个神奇的东东,共有3种方式可实现深拷贝,来源

objectMapper 工具类初始化
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

@Slf4j
@Component
public class JacksonUtils {
    @Autowired
    ObjectMapper objectMapper;
    private static JacksonUtils MAPPER;

    /**
     * 非 Controller 层使用 @Autowired 方式注入 spring 初始化好的全局单例  objectMapper
     * 无需自己手动 new ObjectMapper();
     */
    @PostConstruct
    public void init(){
        MAPPER = this; // JacksonUtils 类的单例 bean
        MAPPER.objectMapper = this.objectMapper; // spring启动过程中,会暴露该 objectMapper 单例
    }
}
1. 对象 <===> json字符串 互相转换 (下策)
2. 对象 <===> jsonNode 互相转换 (中策)
3. 对象 <===> TokenBuffer 互相转换 (上策)
/**
 * 不同class类型拷贝
 * @Param clazz 这个类,必须有1个默认的构造 方法,这是使用 Jackson 正反序列化必须的; 
 * 如果是Gson 进行正反序列化,则没有该 构造方法 的要求 : 
 * Gson 参考 https://www.baeldung.com/java-deep-copy#2-json-serialization-with-gson
 */
@SneakyThrows
public static <T> T deepClone(Object javaObj, Class<T> clazz) {
    ObjectMapper mapper = MAPPER.objectMapper;

    // method 1 : json string 🚗 . 速度最慢,但是兼容性较好
    if(javaObj instanceof String){
      return mapper.readValue(mapper.writeValueAsString(javaObj), targetClass);
    }

    // method 2 : jsonNode tree 🛫
    // return mapper.treeToValue(mapper.valueToTree(javaObj), clazz);

    // method 3 : token buffer 🚀
    TokenBuffer tb = new TokenBuffer(mapper, false);
    mapper.writeValue(tb, javaObj);
    return mapper.readValue(tb.asParser(), clazz);
}

/**
 * 相同class类型拷贝
 */
@SneakyThrows
public static <T> T deepClone(T source) {
    return (T) deepClone(source, source.getClass());
}

初始化工具类,来源todo
深拷贝代码参考来源

<think>我们正在讨论如何正确实现Java对象深拷贝deep clone)。根据用户提供的引用和之前关于Object类源码的分析,我们知道Object.clone()方法默认是浅拷贝。要实现深拷贝,需要递归地复制对象中的所有引用类型成员。 引用[1]和[2]都指出了深拷贝与浅拷贝的区别:浅拷贝只复制基本数据类型和引用(即指向同一个对象),而深拷贝会复制所有引用类型成员所指向的对象,从而创建一个完全独立的副本。 引用[3]则提供了一些深拷贝的实现方案,包括使用序列化(如JSON序列化)和工具类。 下面我们将详细说明如何正确实现深拷贝: 方法一:重写clone方法(递归调用clone) 1. 让类实现Cloneable接口(标记接口)。 2. 重写clone()方法,将访问修饰符从protected改为public。 3. 在clone方法中,先调用super.clone()得到浅拷贝对象。 4. 对于每个引用类型的成员变量,调用其clone()方法(如果该成员变量也支持深拷贝)或者通过其他方式创建新对象。 5. 注意:如果成员变量是数组,可以使用数组的clone方法(数组本身实现了Cloneable)或者手动创建新数组并复制每个元素。 方法二:使用拷贝构造方法(Copy Constructor)或拷贝工厂 1. 在类中定义一个构造方法,参数为该类的一个对象。 2. 在构造方法中,为每个成员变量创建新的对象(对于引用类型,递归调用其拷贝构造方法或clone方法)。 方法三:使用序列化(Serialization) 1. 将对象序列化为字节流,然后再反序列化成一个新的对象。这样得到的新对象与原对象完全独立。 2. 要求对象及其所有成员变量都必须实现Serializable接口。 3. 可以使用不同的序列化工具,如Java原生序列化、JSON序列化(如Jackson)等。 引用[3]中特别提到了使用Jackson进行JSON序列化实现深拷贝,并给出了三种策略(下中上策): - 下策:对象与JSON字符串互相转换(性能较低,因为字符串操作开销大) - 中策:对象与JsonNode互相转换(避免字符串操作,直接操作树结构) - 上策:对象与TokenBuffer互相转换(更高效,流式处理) 下面我们分别给出示例: 示例1:重写clone方法实现深拷贝 ```java public class Person implements Cloneable { private String name; // String是不可变对象,可以不用深拷贝(但为了演示,我们当作需要深拷贝) private Address address; // 引用类型 // 省略构造方法、getter/setter @Override public Person clone() throws CloneNotSupportedException { Person cloned = (Person) super.clone(); // 对引用类型成员进行深拷贝 cloned.address = this.address.clone(); // 这里假设Address也实现了Cloneable并重写了clone方法 return cloned; } } public class Address implements Cloneable { private String city; @Override public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } } ``` 示例2:使用拷贝构造方法 ```java public class Person { private String name; private Address address; public Person(Person other) { this.name = other.name; // String不可变,直接赋值 this.address = new Address(other.address); // 使用Address的拷贝构造方法 } } public class Address { private String city; public Address(Address other) { this.city = other.city; } } ``` 示例3:使用Jackson序列化实现深拷贝(上策:TokenBuffer) ```java import com.fasterxml.jackson.databind.*; public class DeepCopyUtil { private static final ObjectMapper objectMapper = new ObjectMapper(); public static <T> T deepCopy(T object) throws IOException { // 使用TokenBuffer作为中间人 TokenBuffer tokenBuffer = new TokenBuffer(objectMapper, false); objectMapper.writeValue(tokenBuffer, object); JsonParser parser = tokenBuffer.asParser(); return objectMapper.readValue(parser, (Class<T>) object.getClass()); } } ``` 注意:使用序列化方法时,需要确保类有无参构造方法(如果使用Jackson默认配置),并且所有字段都可以被序列化(没有transient和非public字段等限制)。 总结: 1. 如果对象结构简单且层次固定,重写clone方法是最直接的方式。 2. 如果对象结构复杂,或者不想依赖Cloneable接口,可以使用拷贝构造方法。 3. 如果对象结构非常复杂,且需要避免手动编写深拷贝代码,使用序列化(特别是Jackson的TokenBuffer)是高效且安全的选择。 最后,引用[3]特别提醒不要使用Spring或Apache Commons BeanUtils的copy方法,因为这些工具类实现的是浅拷贝。 因此,正确实现深拷贝需要根据具体场景选择合适的方法,并确保所有引用类型成员都被正确复制。 引用说明:[^1][^2][^3]</think>### Java 对象深拷贝Deep Clone)实现指南 在 Java 中实现对象深拷贝需要完全复制对象及其所有嵌套对象,确保原始对象与副本完全独立。以下是三种主流实现方案: --- #### 方法一:重写 `clone()` 方法(基础方案) ```java class Address implements Cloneable { String city; @Override protected Address clone() throws CloneNotSupportedException { return (Address) super.clone(); // 浅拷贝 } } class User implements Cloneable { String name; Address address; @Override public User clone() throws CloneNotSupportedException { User cloned = (User) super.clone(); cloned.address = this.address.clone(); // 关键:手动深拷贝引用类型 return cloned; } } // 使用示例 User original = new User("Tom", new Address("New York")); User deepCopy = original.clone(); ``` **关键点**: 1. 实现 `Cloneable` 标记接口 2. 重写 `clone()` 方法(提升为 `public`) 3. **递归调用**所有引用类型成员的 `clone()` 方法[^1] 4. 数组类型需特殊处理:`cloned.array = original.array.clone()` **局限**: - 需要修改类源码 - 嵌套层级深时代码冗长 - 循环引用会导致栈溢出 --- #### 方法二:序列化方案(推荐方案) ```java import com.fasterxml.jackson.databind.ObjectMapper; class DeepCopyUtil { private static final ObjectMapper objectMapper = new ObjectMapper(); // Jackson 序列化实现深拷贝 public static <T> T deepCopy(T obj) throws IOException { // 上策:TokenBuffer 流式处理(高效) TokenBuffer buffer = new TokenBuffer(objectMapper, false); objectMapper.writeValue(buffer, obj); return objectMapper.readValue(buffer.asParser(), (Class<T>) obj.getClass()); // 中策:JsonNode 转换 // JsonNode node = objectMapper.valueToTree(obj); // return objectMapper.treeToValue(node, obj.getClass()); // 下策:JSON 字符串中转(性能最低) // String json = objectMapper.writeValueAsString(obj); // return objectMapper.readValue(json, obj.getClass()); } } // 使用示例 User original = new User("Tom", new Address("New York")); User deepCopy = DeepCopyUtil.deepCopy(original); ``` **优势**: 1. **无需修改类源码**(只需实现 `Serializable`) 2. 自动处理复杂嵌套和循环引用 3. Jackson 的 `TokenBuffer` 方案性能最优(避免字符串转换)[^3] 4. 支持泛型对象 **注意**: - 使用 Jackson 需添加依赖:`com.fasterxml.jackson.core:jackson-databind` - 避免使用 Gson/Fastjson(类型擦除和精度问题)[^3] - 类需有无参构造器 --- #### 方法三:拷贝构造器/工厂方案 ```java class Address { String city; // 拷贝构造器 Address(Address other) { this.city = other.city; } } class User { String name; Address address; // 深拷贝工厂方法 static User deepCopy(User other) { User copy = new User(); copy.name = other.name; copy.address = new Address(other.address); // 创建新对象 return copy; } } ``` **适用场景**: - 不可变对象(如 String、Integer) - 简单数据结构 - 需要精细控制拷贝过程时 --- #### 方案对比 | 特性 | `clone()` 重写 | 序列化方案 | 拷贝构造器 | |--------------------|-----------------------|----------------------|------------------| | 修改源码需求 | 需要 | **不需要** | 需要 | | 循环引用处理 | 不支持 | **支持** | 不支持 | | 性能 | 高 | 中(Jackson 优化后) | 高 | | 泛型支持 | 有限 | **完善** | 有限 | | 第三方依赖 | 无 | Jackson | 无 | > **终极建议**:生产环境优先选择 **Jackson TokenBuffer 序列化方案**(上策),兼顾性能与可靠性[^3]。关键服务建议补充单元测试验证深拷贝效果。 --- ### 深拷贝验证方法 ```java // 验证深拷贝成功 assert deepCopy != original; // 不同对象 assert deepCopy.address != original.address; // 嵌套对象地址不同 deepCopy.address.setCity("London"); // 修改副本 assert "New York".equals(original.address.getCity()); // 原对象不变 ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值