← 返回题目列表

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

中等 第 22 / 32 题 更新于 2026/07/25
对象拷贝cloneJava 基础

简化版

浅拷贝只复制最外层对象,里面的引用字段仍指向同一批子对象;深拷贝会把可变子对象也一并复制,得到两个互不影响的对象图。判断标准就一句:改了拷贝的嵌套字段,原对象会不会跟着变——会变就是浅拷贝。

详细版

先厘清三件容易混的事:

  • 赋值 User b = a;:根本没拷贝,两个变量指向同一个对象。
  • 浅拷贝:new 了一个新的外层对象,但它的引用字段(ListAddress 等)还是指向原来的子对象。
  • 深拷贝:连同引用字段指向的可变对象一起复制,两份数据彻底独立。

Object.clone() 默认做的就是浅拷贝——按字段逐个复制,基本类型复制值,引用类型复制的是「引用」,所以子对象仍是共享的。而且它还牵扯 Cloneable 标记接口、受保护的 clone 方法、绕过构造器等历史包袱,业务代码里通常不如显式复制清晰。

// 手写深拷贝:把可变的 Address 也 new 一份
User copy = new User(
    original.getName(),                             // String 不可变,直接共享即可
    new Address(original.getAddress().getCity())    // 可变对象,必须复制
);

注意 nameString,不可变,直接共享没有任何风险;只有可变的子对象才需要复制。所以更好的设计往往是让子对象不可变,从根上省掉深拷贝的麻烦。

完整版教学

一、为什么会有「浅」这个坑

Java 里变量存的是引用(对象的地址),不是对象本身。所以「复制一个对象」天然有歧义:复制那个地址,还是复制地址指向的东西?浅拷贝复制了地址,深拷贝复制了东西。

一个典型 bug:

List<Integer> a = new ArrayList<>(List.of(1, 2, 3));
List<Integer> b = new ArrayList<>(a);   // 拷贝构造,但对元素是浅拷贝

这里 b 是新的 List,往 b 加元素不影响 a——外层独立了。但如果元素是可变对象(比如 List<User>),b.get(0)a.get(0) 还是同一个 User,改其中一个另一个也变。这就是为什么「拷贝了 List 还出 bug」。

二、几种实现深拷贝的方式

  • 手写构造 / 拷贝方法:最清晰可控,逐字段决定复制还是共享。推荐首选。
  • 拷贝构造函数new ArrayList<>(old)new HashMap<>(old)——注意它们只深拷贝容器结构,元素仍是浅拷贝。
  • clone():默认浅拷贝;要深拷贝得重写 clone()、手动复制每个可变字段,很容易漏,不推荐。
  • 序列化再反序列化:把对象写成字节流再读回来,天然是整棵对象图的深拷贝。但要求所有字段可序列化、性能差、还有安全问题,只在没有更好办法时用。

三、关键判断:这个子对象「可变」吗

深拷贝的目的是隔离修改。如果一个子对象根本不可变(StringIntegerLocalDate、你自己设计的不可变值对象),它被共享也永远不会被改,那共享它就是安全的,不需要深拷贝。

记忆点:深拷贝只针对「可变且会被独立修改」的子对象。能把子对象设计成不可变的,就根本不需要深拷贝——这是最优解,而不是无脑给所有东西都做深拷贝。

四、用对象图理解复制边界

深拷贝复制的不是“字段列表”,而是从根对象可达的一张对象图。假设一个订单有 2 个明细,而两个明细共享同一个商品对象;正确的深拷贝既要让新订单拥有独立明细,又要决定新图内部是否继续共享同一个商品,不能把同一商品错误复制成两个不同对象。

原订单 ─┬→ 明细A ─┐
       └→ 明细B ─┴→ 商品P

新订单 ─┬→ 新明细A ─┐
       └→ 新明细B ─┴→ 新商品P(若商品可变且要求隔离)

对象图还可能有环,例如 parent.child.parent == parent。朴素递归会无限复制,因此通用深拷贝算法需要一张“原对象 → 新对象”的身份映射表:每创建一个副本先登记,再递归复制邻接对象。序列化框架也需要用类似的引用追踪机制来保持环和共享关系。

五、方案选择与成本

假设一个对象图有 1 万个可变节点,完整深拷贝至少要访问并分配这 1 万个节点,时间与额外空间通常都是 O(V + E);浅拷贝外层对象则接近 O(字段数)。因此深拷贝绝不是免费的“更安全版本”,复制大图会显著增加延迟、内存峰值和 GC 压力。

方案优点代价与适用边界
手写拷贝构造器类型安全、复制边界清楚字段变化时要同步维护
clone()语言内置入口默认浅拷贝、API 历史包袱重
序列化往返可复制整张对象图慢、受格式和安全约束
不可变对象共享无需复制且线程安全更新时要创建新值
写时复制读多写少时节省成本首次写入成本高,实现更复杂

工程上应先定义隔离边界,再选择方案。DTO 快照可能只需复制可变集合,领域聚合则可能要求整棵聚合内部独立;没有边界定义,“深拷贝”这个需求本身就是含糊的。

六、常见误区与追问

  • 误区:以为 new ArrayList<>(list) 就是深拷贝。 它只复制容器,元素引用仍然共享。
  • 误区:以为调用 super.clone() 会递归复制字段。 它只做逐字段复制,可变引用字段必须显式处理。
  • 误区:给 StringLocalDate 等不可变值也创建副本。 共享不可变对象没有修改泄漏,重复复制只增加成本。
  • 追问:深拷贝一定要复制所有对象吗? 不一定,关键是复制隔离边界内的可变对象,不可变对象和明确共享的服务对象可以保留引用。
  • 追问:有循环引用时如何避免无限递归? 使用 IdentityHashMap 一类按对象身份记录“已复制对象”,遇到同一源对象直接复用对应副本。
  • 追问:为什么 JSON 往返不总是好方案? 它可能丢失具体子类型、对象身份、瞬态字段或环,并且有序列化性能与安全成本。

处理环和共享节点的核心算法可以写成下面的伪代码。关键步骤是“先登记副本,再递归字段”,否则 A 指向 B、B 又指向 A 时,递归永远回不到已知结果:

Object copy(Object source, IdentityHashMap<Object, Object> seen) {
    if (source == null || isImmutable(source)) return source;
    if (seen.containsKey(source)) return seen.get(source);

    Object target = allocateSameType(source);
    seen.put(source, target); // 必须先登记,才能截断环
    for (Field field : fieldsOf(source)) {
        field.set(target, copy(field.get(source), seen));
    }
    return target;
}

真实实现还要处理数组、集合、代理、不可访问字段和构造不变量,所以业务代码通常不应自己造一个“万能深拷贝器”,而应为明确的领域边界编写显式复制逻辑。

七、加强记忆

先画对象图,再决定复制边界:赋值只复制根引用,浅拷贝创建新外壳但共享内部节点,深拷贝则为需要隔离的可变节点建立新图,同时保留必要的共享和环。实践中优先把值对象设计为不可变,让它们安全共享;确需独立修改时,用显式拷贝构造器清楚表达哪些字段复制、哪些字段共享。这样既不会漏掉嵌套可变对象,也不会为“看起来更彻底”付出无谓的时间和内存。