《计算机“十万个为什么”》之 🧩 深浅拷贝 & 引用拷贝:内存世界的复制魔法 ✨

127 阅读15分钟

《计算机“十万个为什么”》之 🧩 深浅拷贝 & 引用拷贝:内存世界的复制魔法 ✨

深拷贝与浅拷贝是内存世界中的两种复制方式,它们各有其独特的特征和适用场景。在 Java 中,深拷贝和浅拷贝的实现方式各有不同,需要根据具体情况进行选择。

本文将介绍深拷贝与浅拷贝的核心特征、Java 中的实现方式、适用场景以及注意事项。

作者:无限大

推荐阅读时间:15min

引言:为什么复制也会出问题? 🤔

在编程世界里,我们经常需要复制数据。但你是否遇到过这样的情况:明明修改了新变量,旧变量却跟着一起变了?或者反过来,以为修改了新变量,结果旧变量纹丝不动?这背后其实隐藏着内存复制的奥秘——深浅拷贝与引用拷贝。

今天,就让我们一起揭开这个内存世界的复制魔法吧!🔮


一、变量与内存:数据存储的基本原理 🧠

在了解拷贝之前,我们首先要明白变量在内存中是如何存储的。

1.1 基本数据类型 vs 引用数据类型

计算机中的数据类型可以分为两大类,这一分类在 Java 语言中尤为重要:

类型特点传递方式Java 常见例子存储方式
基本数据类型不可再分的原子值值传递int, double, boolean, char, byte, short, long, float直接存储在栈内存中
引用数据类型由多个值构成的对象引用传递String, Object, Array, List, Map, 自定义类(Java 当中的包装类都是引用数据类型)栈内存存储引用地址,堆内存存储实际数据

📌 Java 特殊说明: String 虽然是引用数据类型,但在 Java 中具有值类型的特性,因为它是不可变的(immutable)。当你修改 String 对象时,实际上会创建一个新的 String 对象;若要对 String 对象进行频繁修改,建议使用 StringBuilder(非线程安全)或 StringBuffer(线程安全)类以避免不必要的内存开销和性能损耗

1.2 内存分配:栈内存 vs 堆内存

在 Java 中,内存主要分为栈内存和堆内存:

  • 栈内存:用于存储方法调用时的局部变量、方法参数、返回值等。它的内存分配和释放速度很快,因为栈内存的管理是自动的(遵循"先进后出"原则)。
  • 堆内存:用于存储对象实例、数组等动态分配的内存。堆内存的管理相对复杂,因为需要手动分配和释放内存,且垃圾回收机制会自动回收不再使用的对象。

📝 Java 内存模型要点

  • 基本数据类型直接存储在栈内存中,操作简单快速
  • 引用数据类型在栈内存中存储引用地址,堆内存中存储实际数据
  • 引用数据类型的赋值操作是引用拷贝,而非值拷贝
  • 引用数据类型的比较操作是比较引用地址,而非内容
  • 引用数据类型的修改会影响所有指向该对象的引用变量

二、引用拷贝:看似复制,实则共享 🔗

引用拷贝是最简单也最容易让人迷惑的拷贝方式。

2.1 什么是引用拷贝?

引用拷贝只是复制了对象的引用地址,而不是实际数据。这意味着新变量和原变量指向堆内存中的同一个对象。

2.2 代码示例:Java 中的引用拷贝

在 Java 中,对象赋值操作默认就是引用拷贝,这是初学者最容易混淆的概念之一:

// 创建一个自定义类
class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

/**
 * 引用拷贝示例类
 * 演示Java中对象赋值默认是引用拷贝,而非值拷贝
 */
public class ReferenceCopyExample {
    public static void main(String[] args) {
        // 创建原始对象实例
        Person original = new Person("Tom", 20);

        // 引用拷贝 - 仅复制对象引用,不复制实际对象
        // 此时original和copy指向堆内存中的同一个Person对象
        Person copy = original;

        // 修改拷贝对象的属性值
        // 由于是引用拷贝,original和copy指向同一对象,所以两者都会受到影响
        copy.age = 21;

        // 验证原对象也被修改
        System.out.println(original.age); // 输出: 21 - 原对象年龄也被修改
        System.out.println(copy.age);     // 输出: 21 - 拷贝对象年龄

        // 使用==运算符比较对象引用是否相同
        // 结果为true,证明两个引用指向同一个对象
        System.out.println(original == copy); // 输出: true(比较引用地址)
    }
}

📝 Java 引用拷贝要点

  • 使用 =操作符为对象赋值时,永远是引用拷贝
  • ==运算符比较的是对象引用地址,而非内容
  • 基本数据类型使用 =时是值拷贝,而非引用拷贝

==equals的区别

在基本数据类型当中 == 运算符用于比较两个值是否相等,并且没有 equals方法。

在引用数据类型中,==equals 的区别尤为重要: 在 Java 中,==equals 是两个常用的比较操作符,但它们有着本质的区别: ==运算符比较的是对象的引用地址,而 equals方法比较的是对象的内容。

2.3 引用拷贝的内存变化

graph TD
    subgraph 栈内存 Stack
        A[original] --> B[引用地址: 0x123]
        C[copy] --> B
    end

    subgraph 堆内存 Heap
        D[地址 0x123] --> E{{name: 'Tom' \n age: 21}}
    end

三、浅拷贝:表面复制,深层共享 🚰

浅拷贝比引用拷贝进了一步,但仍有局限。

3.1 什么是浅拷贝?

浅拷贝会创建一个新对象,并复制原对象的所有基本数据类型属性。但对于引用类型属性,它只复制引用地址,而不是实际数据。

3.2 Java 中的浅拷贝实现方式

Java 没有内置的浅拷贝函数,需要通过以下方式实现:

方法实现方式适用场景
Cloneable 接口实现 Cloneable 并重写 clone()方法自定义对象浅拷贝
构造方法拷贝新建对象并逐个复制基本类型属性简单对象浅拷贝
Arrays.copyOf()Arrays.copyOf(original, length)数组浅拷贝
Collections.copy()Collections.copy(dest, src)集合浅拷贝

3.3 代码示例:Java 中的浅拷贝

// 地址类
class Address {
    String city;
    String country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

// 人员类 - 实现Cloneable接口以支持浅拷贝
class Person implements Cloneable {
    String name; // 基本类型包装类(String是不可变引用类型)
    int age;     // 基本数据类型
    Address address; // 引用数据类型

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

    /**
     * 重写clone()方法实现浅拷贝
     * Object类的clone()方法默认实现浅拷贝
     * @return 拷贝的Person对象
     * @throws CloneNotSupportedException 如果未实现Cloneable接口会抛出此异常
     */
    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

/**
 * 浅拷贝示例类
 * 演示浅拷贝对基本类型和引用类型的不同处理方式
 */
public class ShallowCopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 创建地址对象(引用类型)
        Address address = new Address("Beijing", "China");
        // 创建原始人员对象
        Person original = new Person("Tom", 20, address);

        // 使用clone()方法进行浅拷贝
        Person shallowCopy = original.clone();

        // 修改拷贝对象的基本类型属性(age是基本类型)
        shallowCopy.age = 21;
        // 修改拷贝对象的引用类型属性(address是引用类型)
        shallowCopy.address.city = "Shanghai";

        // 基本数据类型属性修改不影响原对象
        System.out.println(original.age); // 输出: 20 - 原对象年龄不变
        System.out.println(shallowCopy.age); // 输出: 21 - 拷贝对象年龄已修改

        // 引用数据类型属性修改影响原对象
        System.out.println(original.address.city); // 输出: Shanghai - 原对象地址被修改
        System.out.println(shallowCopy.address.city); // 输出: Shanghai - 拷贝对象地址

        // 验证是否为不同对象
        System.out.println(original == shallowCopy); // 输出: false - 两个是不同对象
        System.out.println(original.address == shallowCopy.address); // 输出: true - 引用类型属性指向同一对象
        System.out.println(original.name == shallowCopy.name); // 输出: true - String是不可变对象,共享引用
        // 验证对象内容
        System.out.println(original.name.equals(shallowCopy.name)); // 输出: true
        System.out.println(original.address.city.equals(shallowCopy.address.city)); // 输出: true
    }
}

📘 浅拷贝关键点

  • Java 的 Object.clone()方法默认实现的是浅拷贝
  • 要实现浅拷贝,类必须实现 Cloneable接口
  • 浅拷贝对于 String 类型表现特殊,因为 String 是不可变的,修改时会创建新对象而非修改原有对象

3.4 浅拷贝的内存变化

graph TD
    subgraph "栈内存 Stack"
        A[original] --> B["引用地址: 0x123"]
        C[shallowCopy] --> D["引用地址: 0x456"]
    end

    subgraph "堆内存 Heap"
        E["地址 0x123"] --> F{{"name: 'Tom' \n age: 20 \n address: 0x789"}}
        G["地址 0x456"] --> H{{"name: 'Tom' \n age: 21 \n address: 0x789"}}
        I["地址 0x789"] --> J{{"city: 'Shanghai' \n country: 'China'"}}
    end

四、深拷贝:完全复制,彻底独立 🌊

深拷贝是最彻底的拷贝方式。

4.1 什么是深拷贝?

深拷贝会创建一个全新的对象,并递归复制原对象的所有属性,包括引用类型属性。这意味着新对象与原对象完全独立,互不影响。

深拷贝核心特征
graph LR
  A[原对象] -- 拷贝 --> B[新对象]
  B --> C[新基本数据副本]
  B --> D[新建指针]
  D --> E[新子对象]
  E --> F[新建数组副本]

4.2 Java 中的深拷贝实现方式

Java 实现深拷贝需要更多工作,常见方法如下:

方法实现方式优点缺点
递归深拷贝手动递归复制所有层级对象灵活可控,无第三方依赖实现复杂,需为每个类编写拷贝逻辑
序列化/反序列化对象转字节流再恢复为新对象实现简单,自动递归性能较差,要求所有对象可序列化
Apache Commons 工具类使用 SerializationUtils.clone()成熟稳定,处理边界情况增加第三方依赖

4.3 代码示例:Java 递归实现深拷贝

import java.util.ArrayList;
import java.util.List;

// 地址类 - 实现深拷贝接口
class Address implements DeepCloneable {
    String city;
    String country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }

    /**
     * 深拷贝方法,创建并返回一个新的Address对象
     * @return 新的Address对象,属性值与当前对象相同
     */
    @Override
    public Address deepClone() {
        return new Address(this.city, this.country);
    }
}

/**
 * 深拷贝接口,定义深拷贝方法
 * 实现此接口的类需要提供深拷贝实现
 */
interface DeepCloneable {
    /**
     * 创建并返回当前对象的深拷贝
     * @return 当前对象的深拷贝
     */
    Object deepClone();
}

// 人员类 - 实现深拷贝
class Person implements DeepCloneable {
    String name;
    int age;
    Address address;
    List<String> hobbies;

    public Person(String name, int age, Address address, List<String> hobbies) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.hobbies = hobbies;
    }

    /**
     * 实现深拷贝方法,递归拷贝所有层级属性
     * @return 新的Person对象,所有属性(包括引用类型)都是拷贝的
     */
    @Override
    public Person deepClone() {
        // 拷贝基本类型属性(直接赋值)
        String newName = this.name; // String是不可变的,直接赋值即可
        int newAge = this.age;

        // 递归拷贝引用类型属性(调用Address的深拷贝方法)
        Address newAddress = this.address.deepClone();

        // 拷贝集合(创建新集合并添加元素)
        List<String> newHobbies = new ArrayList<>();
        for (String hobby : this.hobbies) {
            newHobbies.add(hobby); // String是不可变的,直接添加
        }

        // 返回新创建的Person对象
        return new Person(newName, newAge, newAddress, newHobbies);
    }
}

/**
 * 深拷贝示例类
 * 演示递归实现深拷贝的完整流程和效果
 */
public class DeepCopyExample {
    public static void main(String[] args) {
        // 创建原始对象
        Address address = new Address("Beijing", "China");
        List<String> hobbies = new ArrayList<>();
        hobbies.add("reading");
        hobbies.add("coding");
        Person original = new Person("Tom", 20, address, hobbies);

        // 进行深拷贝 - 创建完全独立的新对象
        Person deepCopy = original.deepClone();

        // 修改拷贝对象的属性
        deepCopy.age = 21;                  // 修改基本类型属性
        deepCopy.address.city = "Shanghai"; // 修改引用类型属性
        deepCopy.hobbies.add("gaming");     // 修改集合类型属性

        // 原对象不受影响(深拷贝特点)
        System.out.println(original.age); // 输出: 20 - 原对象年龄未变
        System.out.println(original.address.city); // 输出: Beijing - 原对象地址未变
        System.out.println(original.hobbies.size()); // 输出: 2 - 原对象集合大小未变

        // 拷贝对象已修改
        System.out.println(deepCopy.age); // 输出: 21 - 拷贝对象年龄已修改
        System.out.println(deepCopy.address.city); // 输出: Shanghai - 拷贝对象地址已修改
        System.out.println(deepCopy.hobbies.size()); // 输出: 3 - 拷贝对象集合已修改
    }
}

4.4 代码示例:使用序列化实现深拷贝

import java.io.*;
import java.util.ArrayList;
import java.util.List;

// 所有需要序列化的类必须实现Serializable接口
class Address implements Serializable {
    String city;
    String country;

    public Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}

class Person implements Serializable {
    String name;
    int age;
    Address address;
    List<String> hobbies;

    public Person(String name, int age, Address address, List<String> hobbies) {
        this.name = name;
        this.age = age;
        this.address = address;
        this.hobbies = hobbies;
    }

    /**
     * 使用序列化实现深拷贝
     * 通过将对象写入字节流再读取回来创建全新对象
     * @return 当前对象的深拷贝
     * @throws IOException 如果序列化过程出错
     * @throws ClassNotFoundException 如果反序列化时类找不到
     */
    public Person deepClone() throws IOException, ClassNotFoundException {
        // 将对象写入字节流(序列化过程)
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);

        // 从字节流读取对象(反序列化过程,创建全新对象)
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);

        return (Person) ois.readObject();
    }
}

/**
 * 序列化深拷贝示例类
 * 演示使用序列化/反序列化实现深拷贝的方法
 */
public class SerializationDeepCopyExample {
    public static void main(String[] args) {
        try {
            // 创建原始对象
            Address address = new Address("Beijing", "China");
            List<String> hobbies = new ArrayList<>();
            hobbies.add("reading");
            hobbies.add("coding");
            Person original = new Person("Tom", 20, address, hobbies);

            // 使用序列化实现深拷贝
            Person deepCopy = original.deepClone();

            // 修改拷贝对象
            deepCopy.age = 21;                  // 修改基本类型属性
            deepCopy.address.city = "Shanghai"; // 修改引用类型属性
            deepCopy.hobbies.add("gaming");     // 修改集合类型属性

            // 验证原对象不受影响
            System.out.println(original.age); // 输出: 20 - 原对象年龄未变
            System.out.println(original.address.city); // 输出: Beijing - 原对象地址未变
            System.out.println(original.hobbies.size()); // 输出: 2 - 原对象集合大小未变

            // 验证拷贝对象已修改
            System.out.println(deepCopy.age); // 输出: 21 - 拷贝对象年龄已修改
            System.out.println(deepCopy.address.city); // 输出: Shanghai - 拷贝对象地址已修改
            System.out.println(deepCopy.hobbies.size()); // 输出: 3 - 拷贝对象集合已修改
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

⚠️ 序列化拷贝注意事项

  • 所有参与序列化的类必须实现 Serializable 接口
  • 静态成员变量不会被序列化
  • 使用 transient 关键字标记的成员不会被序列化
  • 性能比手动递归拷贝差,但实现简单

4.5 深拷贝的内存变化

graph LR
%% ===== 栈内存 =====
subgraph stack[栈内存 Stack]
  orig[original对象] -->|指针| addr_stack1(0x7ffe1100)
  copy[deepCopy对象] -->|指针| addr_stack2(0x7ffe1200)
end

%% ===== 堆内存 =====
subgraph heap[堆内存 Heap]
  %% 原始对象
  addr_stack1 --> obj1[原始对象]
  obj1["name: 'Tom'\nage: 20"] -->|address| heap_addr1(0x1003000)
  obj1 -->|hobbies| heap_addr3(0x1005000)

  %% 拷贝对象
  addr_stack2 --> obj2[拷贝对象]
  obj2["name: 'Tom'\nage: 21"] -->|address| heap_addr2(0x1004000)
  obj2 -->|hobbies| heap_addr4(0x1006000)

  %% 子对象
  heap_addr1 --> addr_obj["地址对象\ncity: 'Beijing'"]
  heap_addr2 --> copy_addr_obj["地址对象\ncity: 'Shanghai'"]

  heap_addr3 --> array1[["爱好数组\n• reading\n• coding"]]
  heap_addr4 --> array2[["爱好数组\n• reading\n• coding\n• gaming"]]
end

%% ===== 样式 =====
classDef stack fill:#E1F5FE,stroke:#0288D1;
classDef heap fill:#E8F5E9,stroke:#388E3C;
classDef obj fill:#FFECB3,stroke:#FFA000;
classDef array fill:#FCE4EC,stroke:#E91E63;
class stack stack
class heap heap
class obj1,obj2,addr_obj,copy_addr_obj obj
class array1,array2 array

五、三种拷贝方式的对比分析 🆚

拷贝方式特点优点缺点适用场景
引用拷贝只复制引用地址,共用数据效率最高,占用空间最少相互影响,容易引发 bug不需要独立对象,只想创建别名时
浅拷贝复制第一层属性,深层共用效率较高,实现简单深层引用类型仍共享对象结构简单,无深层引用类型时
深拷贝完全复制所有层级完全独立,互不影响效率较低,占用空间大需要完全独立的对象,有深层嵌套结构时

六、实际开发中的拷贝策略 💡

6.1 何时使用哪种拷贝方式?

  • 引用拷贝:函数参数传递、临时变量等不需要修改原对象的场景
  • 浅拷贝:简单数据结构、状态管理中的不可变状态更新
  • 深拷贝:复杂嵌套对象、需要完全隔离的数据

6.2 Java 拷贝中的常见问题与解决方案

问题 1:循环引用处理

Java 的序列化机制无法直接处理循环引用,会抛出 StackOverflowError:

/**
 * 节点类,用于演示序列化深拷贝中的循环引用问题
 * 实现Serializable接口,但不处理循环引用
 */
class Node implements Serializable {
    // 节点数据
    String data;
    // 下一个节点引用
    Node next;

    // 构造方法,初始化节点数据
    public Node(String data) {
        this.data = data;
    }
}

/**
 * 循环引用示例类
 * 演示序列化深拷贝无法处理循环引用的问题
 */
public class CircularReferenceExample {
    public static void main(String[] args) {
        // 创建两个节点
        Node node1 = new Node("A");
        Node node2 = new Node("B");

        // 创建循环引用(node1指向node2,node2指向node1)
        node1.next = node2;
        node2.next = node1;

        // 尝试序列化会抛出StackOverflowError
        try {
            serializeAndDeserialize(node1);
        } catch (Exception e) {
            e.printStackTrace(); // 会抛出StackOverflowError,因为序列化无法处理循环引用
        }
    }
}

解决方案:手动控制拷贝过程,使用标识避免无限递归:

/**
 * 节点类,实现DeepCloneable接口处理循环引用
 * 使用标记变量解决深拷贝中的循环引用问题
 */
class Node implements DeepCloneable {
    // 节点数据
    String data;
    // 下一个节点引用
    Node next;
    // 标记是否正在克隆,用于处理循环引用
    transient boolean isCloning = false;

    // 构造方法,初始化节点数据
    public Node(String data) {
        this.data = data;
    }

    /**
     * 深拷贝方法,处理循环引用
     * 使用isCloning标记避免无限递归
     * @return 深拷贝的节点对象
     */
    @Override
    public Node deepClone() {
        // 如果正在克隆过程中遇到自己,直接返回当前对象(处理循环引用)
        if (isCloning) {
            return this;
        }

        // 标记开始克隆
        isCloning = true;
        // 创建新节点
        Node clone = new Node(this.data);
        // 递归克隆下一个节点
        if (this.next != null) {
            clone.next = this.next.deepClone();
        }
        // 克隆完成,重置标记
        isCloning = false;

        return clone;
    }
}
问题 2:不可变对象的特殊处理

Java 中的 String、Integer 等不可变对象有特殊的拷贝行为:

/**
 * 不可变对象拷贝示例类
 * 演示不可变对象(如String)的特殊拷贝行为
 */
public class ImmutableCopyExample {
    public static void main(String[] args) {
        // 创建原始字符串(不可变对象)
        String original = "Hello";
        // 引用拷贝 - 指向同一个String对象
        String copy = original;

        // String是不可变的,修改操作会创建新对象而非修改原有对象
        copy = copy + " World"; // 实际上创建了新的String对象

        // 验证原始对象未被修改
        System.out.println(original); // 输出: Hello - 原始对象保持不变
        System.out.println(copy);     // 输出: Hello World - 新创建的对象
        // 验证两个引用指向不同对象
        System.out.println(original == copy); // 输出: false
    }
}

📌 Java 拷贝最佳实践

  • 优先考虑不可变对象设计,减少拷贝需求
  • 对于简单对象,使用构造函数进行显式拷贝
  • 对于复杂对象,考虑使用 Builder 模式创建新对象
  • 深拷贝成本高,谨慎使用,优先考虑设计上避免深拷贝需求

七、总结与思考 🤔

深浅拷贝和引用拷贝是 JavaScript(以及其他面向对象语言)中非常基础但又极其重要的概念。理解它们的工作原理,能够帮助我们编写更健壮、更可预测的代码。

  • 引用拷贝:快速但危险,像共享一个房间的钥匙
  • 浅拷贝:表面独立但深层共享,像复制了房子但共享地下室
  • 深拷贝:完全独立但代价高昂,像建造一座一模一样的新房子

在实际开发中,我们需要根据具体场景和性能需求,选择合适的拷贝方式,而不是一味追求最"安全"的深拷贝。


八、延伸阅读 📚

  1. MDN Web Docs - 结构化克隆算法
  2. Lodash - cloneDeep

希望这篇文章能帮助你理解计算机世界中的"复制魔法"!如果有任何问题或想法,欢迎在评论区留言讨论哦~ 😊