深拷贝和浅拷贝的区别?

#Java#面试八股

深拷贝和浅拷贝的区别?

结论:浅拷贝(shallow copy)只复制对象本身和其中的基本类型字段,引用类型字段仍指向同一个对象;深拷贝(deep copy)还要递归复制引用指向的对象,得到的两份数据完全独立,改其中一个不影响另一个。判断标准很简单:拷贝之后修改副本里的引用字段,看原对象会不会跟着变。

1. 内存结构上的差别

原对象:u1 ──→ User{name="Tom", addr=0x11} ──→ Address{city="杭州"}   ← 0x11

浅拷贝:u2 ──→ User{name="Tom", addr=0x11} ──→ 同一个 Address(共享)
深拷贝:u3 ──→ User{name="Tom", addr=0x22} ──→ Address{city="杭州"}   ← 新对象

注:String、Integer 等不可变(immutable)对象即使被共享也安全

注意 String 这类字段:浅拷贝共享的是同一个 String 引用,但 String 不可变,任何"修改"都会产生新对象,所以共享不可变对象不构成浅拷贝风险。风险只出现在可变引用字段上:List、Map、数组、自定义 Bean。

2. 四种实现方式

方式深浅优点缺点
Object.clone() 默认实现浅简单、性能好必须实现 Cloneable,引用字段仍需手动处理
拷贝构造器 / 工厂方法可控意图清晰、可逐字段深拷字段多时样板代码多
序列化往返(Serializable/JSON)深递归自动完成要求全链路可序列化,性能差,可能丢类型
不可变对象 + 共享无需拷贝零成本需要设计成不可变

Object.clone() 的语义常被误解:它按字段做逐位复制(field-by-field),基本类型复制值、引用类型复制地址,所以默认是浅拷贝。数组的 clone() 也是浅拷贝(元素引用被复制)。

3. 代码:从浅到深

class Address implements Cloneable {
    String city;
    Address(String city) { this.city = city; }

    @Override protected Address clone() {
        try {
            return (Address) super.clone();     // 字段只有 String,浅拷也安全
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}

class User implements Cloneable {
    String name;
    Address addr;

    User(String name, Address addr) {
        this.name = name;
        this.addr = addr;
    }

    /** 深拷贝:先 super.clone(),再手动递归引用字段 */
    @Override public User clone() {
        try {
            User copy = (User) super.clone();   // 第 1 步:浅拷贝
            copy.addr = addr.clone();           // 第 2 步:替换为独立副本
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}

public class CopyDemo {
    public static void main(String[] args) {
        User u1 = new User("Tom", new Address("杭州"));
        User u2 = u1.clone();
        u2.addr.city = "上海";
        System.out.println(u1.addr.city);       // 杭州,深拷贝互不影响
    }
}

如果省掉 copy.addr = addr.clone(),上面输出会变成"上海"——这就是浅拷贝的经典事故。

4. 序列化实现深拷贝

public static <T extends Serializable> T deepCopy(T obj) {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
         ObjectOutputStream oos = new ObjectOutputStream(bos)) {
        oos.writeObject(obj);
        try (ObjectInputStream ois =
                 new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
            @SuppressWarnings("unchecked")
            T copy = (T) ois.readObject();
            return copy;
        }
    } catch (IOException | ClassNotFoundException e) {
        throw new IllegalStateException("deep copy failed", e);
    }
}

它省去了逐字段处理,代价是:对象图里所有类都要实现 Serializable、transient 字段不会被复制、性能比手写慢一个数量级,而且会绕过构造器。JSON 往返(Jackson objectMapper.readValue(writeValueAsBytes(obj), Type))本质相同,但更容易丢类型信息与日期格式。

5. 常见追问

追问回答要点
BeanUtils.copyProperties 是深拷贝吗?不是,Spring 与 Apache 的实现都是浅拷贝;改副本的引用字段会影响原对象
List.clone() 是深拷贝吗?不是,只复制了元素引用;要深拷得逐个元素 clone 或序列化
深拷贝遇到循环引用怎么办?手写递归会栈溢出,需要 IdentityHashMap 记录已拷贝对象;序列化方式由 JVM 自己处理
不可变对象还需要深拷贝吗?不需要,直接共享;这也是为什么推荐把值对象设计成不可变
record 的拷贝构造器是深拷贝吗?不是,record 的拷贝构造器只是逐字段赋值,引用字段仍共享
什么时候用浅拷贝?引用字段不可变、或业务上明确允许共享时;浅拷贝性能最好

还有一个细节:Cloneable 是标记接口(marker interface),不实现它调用 clone() 会抛 CloneNotSupportedException;clone() 在 Object 中是 protected,子类需要提升可见性才能被外部调用。

小结

浅拷贝复制"壳",深拷贝复制"整棵树"。实现优先用拷贝构造器(清晰可控),需要递归复制时再考虑序列化;日常开发中更推荐用不可变对象从源头消灭拷贝需求。

笔记加载中…