在 Java 里,“拷贝对象”经常会遇到一个坑:拷贝出来的新对象,改了它的字段,结果原对象也跟着变了。根本原因通常就是 浅拷贝(Shallow Copy)深拷贝(Deep Copy) 的区别。

什么是浅拷贝?(Shallow Copy)

浅拷贝:只复制对象本身的“第一层”数据。

  • 基本类型(intdouble 等)会直接复制值。
  • 引用类型(如 List、自定义对象)只会复制“引用地址”,新旧对象会指向同一份内部数据。

“浅拷贝”的本质:只复制第一层字段,不复制引用对象内部结构

浅拷贝示例(clone 方式)

class Address {
    String city;

    Address(String city) {
        this.city = city;
    }
}

class Person implements Cloneable {
    String name;      // 第一层字段(引用类型,但指向String不可变对象)
    Address address;  // 第一层字段(引用类型,指向可变Address对象)

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    @Override
    protected Person clone() throws CloneNotSupportedException {
        return (Person) super.clone(); // 只做浅拷贝
    }
}

public class ShallowCopyDemo {
    public static void main(String[] args) throws Exception {
        Address addr = new Address("Shanghai");
        Person p1 = new Person("Alice", addr);

        Person p2 = p1.clone(); // 浅拷贝

        p2.name = "Bob";                // 改第一层字段:不影响 p1
        p2.address.city = "Beijing";    // 改内部引用对象:会影响 p1

        System.out.println(p1.name);            // Alice
        System.out.println(p1.address.city);    // Beijing(被影响了)
        System.out.println(p2.name);            // Bob
        System.out.println(p2.address.city);    // Beijing
    }
}

引用指向图

在这里插入图片描述

结论:浅拷贝拷贝出来的 p2.addressp1.address 指向同一个 Address 对象。

1) 类结构:谁是“第一层字段”,谁是“引用对象”

class Address {
    String city;
}
class Person implements Cloneable {
    String name;      // 第一层字段(引用类型,但指向String不可变对象)
    Address address;  // 第一层字段(引用类型,指向可变Address对象)
}
  • Person 里有两个字段:nameaddress
  • 浅拷贝只复制 Person 这一层的字段值:name 字段里存的是“指向某个 String 的引用”;
    address 字段里存的是“指向某个 Address 的引用”

2) clone() 为啥是浅拷贝⭐

@Override
protected Person clone() throws CloneNotSupportedException {
    return (Person) super.clone(); // 只做浅拷贝
}

//Person p2 = p1.clone();

进入 super.clone():调用的是 java.lang.Object#cloneObject.clone() 是一个 JVM 层面的原生(native)对象复制操作(实现不在 Java 代码里,在 JVM/运行时里)。做的是“按对象内存布局把实例字段整块拷贝”,而不是递归复制引用对象。

  • 它会先做一个关键检查:当前对象是否实现了 Cloneable。实现了:允许克隆; 没实现:抛 CloneNotSupportedException

  • 接下来,JVM 创建一个“同类型的新对象”,但是不会调用构造器不会执行初始化逻辑,只是让 JVM 分配一块足够容纳 Person 实例的内存,得到一个新的 Person 对象壳子。

  • 然后 JVM 会把原对象的 实例字段 拷贝到新对象里,效果等价于:

    • 基本类型字段:直接复制数值(int/long/double …)
    • 引用类型字段:复制的是“引用值”(可以理解为指针/句柄),不会复制引用指向的对象本身
  • 【需要注意的是】,clone() 返回类型是 Object,所以需要 (Person) 强转。

浅拷贝等价伪代码

Object.clone()(浅拷贝)的等价伪代码如下:

// 语义等价伪代码:java.lang.Object#clone()
Object Object_clone(Object thisObj) throws CloneNotSupportedException {
    // A. 权限检查:必须实现 Cloneable
    if (!(thisObj instanceof Cloneable)) {
        throw new CloneNotSupportedException();
    }

    // B. 分配一个“同运行时类型”的新对象:不调用任何构造器
    //    (JVM 内部类似 Unsafe.allocateInstance)
    Object newObj = VM.allocateInstance(thisObj.getClass()); // ctor NOT called

    // C. 复制“实例字段”:按对象布局把内容拷贝过去(浅拷贝的根源)
    //    - 基本类型:复制值
    //    - 引用类型:复制引用值(地址/句柄),不会递归复制引用指向的对象
    VM.copyInstanceFieldsBitwise(src = thisObj, dst = newObj);

    return newObj;
}

把这段套到 Person 上,效果近似于:

Person p2 = VM.allocateInstance(Person.class); // 不跑 Person(...) 构造器
p2.name = p1.name;        // 复制引用
p2.address = p1.address;  // 复制引用(所以共享同一个 Address)

什么是深拷贝?(Deep Copy)

深拷贝:不仅复制对象本身,还会把对象内部所有引用对象也“递归地复制一份”。 因此拷贝后,新对象与原对象内部引用的数据互不影响。

深拷贝示例(手写 clone:对引用字段也 clone)

class Address implements Cloneable {
    String city;

    Address(String city) {
        this.city = city;
    }

    @Override
    protected Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
}

class Person implements Cloneable {
    String name;
    Address address;

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    @Override
    protected Person clone() throws CloneNotSupportedException {
        Person copied = (Person) super.clone();   // 先浅拷贝第一层
        copied.address = address.clone();         // 再深拷贝引用对象
        return copied;
    }
}

public class DeepCopyDemo {
    public static void main(String[] args) throws Exception {
        Person p1 = new Person("Alice", new Address("Shanghai"));
        Person p2 = p1.clone(); // 深拷贝

        p2.address.city = "Beijing";

        System.out.println(p1.address.city); // Shanghai(不受影响)
        System.out.println(p2.address.city); // Beijing
    }
}

引用指向图

在这里插入图片描述

深拷贝等价伪代码

深拷贝版 clone():在“浅拷贝之后”手动再拷一层引用字段

class Address implements Cloneable {
    String city;

    @Override
    protected Address clone() throws CloneNotSupportedException {
        return (Address) super.clone(); // Address 里只有 String,够用了
    }
}

class Person implements Cloneable {
    String name;
    Address address;

    @Override
    protected Person clone() throws CloneNotSupportedException {
        Person copied = (Person) super.clone();      // 先做 JVM 字段拷贝(浅)
        copied.address = address.clone();            // 再把可变引用对象复制一份(深)
        return copied;
    }
}

对应的“等价伪代码”就是:

Person deepClone(Person p1) {
    Person p2 = shallowCloneByVM(p1);      // 等价 super.clone()
    p2.address = shallowCloneByVM(p1.address); // 再克隆引用对象
    return p2;
}

什么时候必须用深拷贝?

一般当对象内部包含可变引用对象时,更需要深拷贝,例如:

  • 字段里有 List / Map / 数组
  • 字段里有可变的自定义对象(如 Address
  • 希望复制出“完全独立”的对象用于后续修改(例如 DTO 转换、缓存快照、回滚等)

如果内部引用对象是不可变的(例如 StringIntegerLocalDateTime 等不可变类型),浅拷贝的风险会小很多。

深拷贝的另一种常见方式:序列化 / 反序列化

除了手写 clone()/拷贝构造器外,序列化深拷贝也很常见:把对象(以及它引用到的整个对象图)写入字节流,再从字节流读回来。读回来的对象是全新的实例,一般能达到“深拷贝”的效果。

原生 Java 序列化实现深拷贝

前提:对象图中所有参与拷贝的类型都要实现 Serializable

import java.io.*;

class Address implements Serializable {
    private static final long serialVersionUID = 1L;

    String city;

    Address(String city) {
        this.city = city;
    }
}

class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    String name;
    Address address;

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
}

public class SerializationDeepCopyDemo {

    public static <T extends Serializable> T deepCopyBySerialization(T obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            try (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 RuntimeException("Deep copy by serialization failed", e);
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("Alice", new Address("Shanghai"));
        Person p2 = deepCopyBySerialization(p1);

        p2.address.city = "Beijing";

        System.out.println(p1.address.city); // Shanghai(不受影响)
        System.out.println(p2.address.city); // Beijing

        // 验证不是同一个引用
        System.out.println(p1.address == p2.address); // false
        System.out.println(System.identityHashCode(p1.address));
        System.out.println(System.identityHashCode(p2.address));
    }
}

和手写深拷贝对比优缺点

  • 优点
    • 写起来省心:对复杂对象图(多层嵌套)不必手动逐层 new
    • 对“共享引用/循环引用”的对象图也通常能正确还原结构(仍保持引用关系)
  • 缺点
    • 性能与内存开销大:序列化/反序列化 + 字节数组
    • 侵入性:需要 Serializable,对象图里任意一个不可序列化都会直接失败(NotSerializableException
    • 可维护性一般:类演进时要考虑 serialVersionUID、序列化兼容等问题
  • 适用建议
    • 偶尔拷贝、对象图复杂、对性能不敏感:可以用
    • 高频拷贝/核心链路:更推荐拷贝构造器、手写深拷贝或专门的映射方案(按业务字段可控)

补充:关于 = 赋值

关于 = 赋值,不是拷贝

Person p2 = p1; // 只是引用指向同一个对象

这不是浅拷贝/深拷贝,而是“同一个对象两个引用”。
在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐