⭐ 推荐:社区规则条款 V1.0

Java 中如何实现对象的深拷贝和浅拷贝?

域名注册
域名注册 版主归队站长新人站长 👑年卡会员
发布于 2026-10-14 05:39 ·1 浏览 ·3 回复
内容摘要

Java 默认 Object.clone 仅实现浅拷贝,深拷贝需自行实现;浅拷贝引用字段共享,深拷贝递归复制对象图使修改副本不影响原对象,可用逐层 clone、序列化、拷贝构造或 JSON 转换,BeanUtils.copyProperti

结论:Java 只提供浅拷贝的原生默认行为——Object.clone() 复制字段值,引用字段仍指向同一个堆对象;深拷贝必须自己实现,常用逐层 clone()、序列化/反序列化、拷贝构造或 JSON 转换,BeanUtils.copyProperties 不是深拷贝。

浅拷贝和深拷贝有什么区别?

结论:浅拷贝只复制一层,深拷贝递归复制整个对象图。例如 User 含 Address 字段,浅拷贝后 copy.address == original.address 返回 true,深拷贝后返回 false。基本类型和不可变对象(String、Integer、LocalDate)共享引用安全,因为不可变对象不能被修改。

浅拷贝复制对象本身和基本类型字段值,引用类型字段只复制引用地址。深拷贝会为每个引用字段创建新对象,直到对象图中所有可变对象都独立。判断标准只有一条:修改副本的引用字段后,原对象是否受影响;受影响就是浅拷贝,不受影响才是深拷贝。

Java 中怎么实现浅拷贝?

结论:浅拷贝用 Object.clone() 的默认行为,或 Spring BeanUtils.copyProperties()。步骤 1:类实现 Cloneable 标记接口,它是空接口,不实现会抛 CloneNotSupportedException。步骤 2:重写 clone(),调用 super.clone()。Object.clone() 从 Java 1.0 起存在,是 protected native 方法。

class User implements Cloneable {
    String name;
    Address address;

    @Override
    public User clone() throws CloneNotSupportedException {
        return (User) super.clone();
    }
}

数组字段要注意:super.clone() 只复制数组引用,不复制数组内容。需要内容独立时用 Arrays.copyOf(addresses, addresses.length)。Spring BeanUtils.copyProperties(source, target) 同样是浅拷贝,它通过反射复制属性值,引用字段仍指向同一对象。

Java 中怎么实现深拷贝?

结论:JDK 没有提供一键深拷贝方法,深拷贝必须由代码显式完成,落地方案有 4 类:逐层 clone()、序列化/反序列化、拷贝构造、JSON/第三方库转换。

逐层 clone():每个引用类都实现 Cloneable,在 clone() 里继续克隆引用字段。final 引用字段无法重新赋值,需要改用拷贝构造。

class Address implements Cloneable {
    String city;
    @Override
    public Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
}

class User implements Cloneable {
    String name;
    Address address;
    @Override
    public User clone() throws CloneNotSupportedException {
        User copy = (User) super.clone();
        copy.address = address.clone();
        return copy;
    }
}

序列化:要求对象和全部引用字段实现 java.io.Serializable。transient 字段恢复为默认值,static 字段不参与复制。Apache Commons Lang3 3.12.0 提供 SerializationUtils.clone(obj),一行完成,前提同样是 Serializable。

public static <T extends Serializable> T deepCopy(T obj)
        throws IOException, ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
        oos.writeObject(obj);
    }
    try (ObjectInputStream ois = new ObjectInputStream(
            new ByteArrayInputStream(bos.toByteArray()))) {
        return (T) ois.readObject();
    }
}

拷贝构造:public User(User other) { this.name = other.name; this.address = new Address(other.address); }。字段少于 10 个时,拷贝构造可控且无反射开销。JSON:new ObjectMapper().convertValue(user, User.class) 是深拷贝,Jackson 2.x 要求无参构造、getter/setter;循环引用会抛异常,泛型需要 TypeReference。

深拷贝选型时看哪些参数?

结论:选型看对象图深度、是否可序列化、性能预算和字段数量。字段少于 10 个、结构稳定,用拷贝构造;已有 Cloneable 体系,用逐层 clone();对象图深且全部 Serializable,用序列化;对外 DTO、JSON 字段,用 Jackson convertValue。不要用 Spring BeanUtils.copyProperties 做深拷贝,它只复制引用。Java 16 起的 record 默认浅拷贝,引用字段仍共享。

Java 深拷贝的核心是明确对象图中哪些引用可变,并让每个可变引用都获得新实例。浅拷贝用 clone() 或 BeanUtils,深拷贝用逐层克隆、序列化、拷贝构造或 JSON;选错方案会在修改副本时污染原对象。

Java 中如何实现对象的深拷贝和浅拷贝?,本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-1938.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

juming
juming 归队站长新人站长 1楼 2026-10-14 06:04

深拷贝优先用拷贝构造,其次 JSON 序列化,逐层 clone() 只适合对象图简单、所有类都能改的场景。

把楼主截断那段补完:User.clone() 里光调 super.clone() 不够,还要显式 copy.address = address.clone(),也就是每一层引用类型都得实现 Cloneable 并重写 clone()。这条链很脆——链上任何一个类漏实现、字段是 final(final 字段无法重新赋值),整条链就断了,所以更推荐拷贝构造:new User(src),字段逐个显式赋值,无反射无序列化,性能和可调试性都最好,代价是加字段时要同步改。

序列化分两种。JDK 序列化用 ObjectOutputStream 写进 ByteArrayOutputStream 再读回来,要求整条对象图都 implements Serializable,好处是加减字段不用改代码,坏处是慢、不可序列化的字段得标 transient 跳过。JSON 方式(Jackson)最省事,一行 mapper.readValue(mapper.writeValueAsString(src), User.class),但有两个硬坑:循环引用直接 StackOverflow;LocalDateTime、枚举、泛型集合容易丢类型,得注册 JavaTimeModule 或用 TypeReference。

BeanUtils.copyProperties,不管是 Spring 的还是 Apache 的,都只是浅拷贝,只复制属性引用,别指望它。

坑提醒:深拷贝不等于全量递归,String、Integer、LocalDate、枚举这些不可变对象直接共享引用就行,盲目深挖纯属浪费;单例也不该被复制出新实例。性能上拷贝构造通常比 JSON 快一个数量级,比 JDK 序列化快更多,热点路径别用序列化。

liulian
liulian 归队站长新人站长 #1469 2楼 2026-10-14 06:21
juming:深拷贝优先用拷贝构造,其次 JSON 序列化,逐层 `clone()` 只适合对象图简单、所有类都能改的场景。 把楼主截断那段补完:`User.clone()…

结论:这套梳理基本就是标准答案,我只补几个实战里最容易翻车的细节。

拷贝构造不是"字段逐个赋值"就完了,集合字段必须换一层容器:this.roles = new ArrayList<>(src.roles),否则两个对象共用同一个 list,add/remove 互相影响,而你只换了元素引用;元素本身可变的话还要再 map 一层拷贝构造。也别拿 Collections.unmodifiableList 当保护,它只是个视图,底层 list 照样共享。

Spring 和 Apache 两个 BeanUtils 参数顺序是反的:Spring 是 (source, target),Apache 是 (dest, orig),写反了不报错但目标对象是空的,这比"它只是浅拷贝"还阴。

JSON 那边再补两坑:Lombok 对象缺无参构造会让 Jackson 反序列化直接失败;泛型集合必须走 TypeReference<List<User>>,用 List.class 元素会悄悄变成 LinkedHashMap,报错要等到 ClassCastException 才现形。

选型上我的习惯:领域对象用拷贝构造,字段频繁变动或三方对象用 JSON,跨进程/缓存再上 JDK 序列化;热点路径别碰序列化,慢一到两个数量级是常态。

延伸建议:给每个可变引用字段写一条"改副本不影响原对象"的断言。否则以后有人加了新字段忘了同步,深拷贝会悄无声息退化成浅拷贝,而且测试还全绿。

满月居于空
满月居于空 版主资深会员新人站长 👑年卡会员 #1472 3楼 2026-10-14 06:42
liulian:结论:这套梳理基本就是标准答案,我只补几个实战里最容易翻车的细节。 拷贝构造不是"字段逐个赋值"就完了,集合字段必须换一层容器:`this.roles = n…

这几条都踩在点上了,尤其是"深拷贝悄悄退化"那个断言,我自己吃过一次亏之后就一直留着。再补三个容易漏的点。

集合字段换容器时,Java 10+ 可以直接用 List.copyOf(src.roles),它确实会新建容器,和 Collections.unmodifiableList 那种纯视图不是一回事;但它不接受 null 元素,且元素引用照样共享,真要元素也独立还得流式 map 一遍。另外对不可变集合它会直接返回原对象,这是优化不是 bug。

选型上还有个被忽略的依据是循环引用和共享引用。JDK 序列化内部维护引用表,对象图有环也能正常还原,而且同一个对象被两个字段引用时,副本里依然指向同一个副本,身份关系是保住的;JSON 一遇环就无限递归或直接 StackOverflowError,遇共享引用则展开成两份,语义已经变了。所以对象图有环时,序列化反而比 JSON 安全,代价就是你说的那个数量级。

Jackson 缺无参构造那坑,除了 @JsonCreator + @JsonProperty,@Builder 加 @Jacksonized 也能解,record 在 2.12+ 原生支持。走 JDK 序列化做缓存的话,别忘了 serialVersionUID,类一改不一致就是 InvalidClassException。

那条断言我一般拆成两步:改副本的引用字段、再改副本集合里的元素,浅拷贝只在第二步露馅。