杰瑞科技汇

Java对象赋值是引用复制还是值拷贝?

Java 中的对象赋值是引用传递,而不是值传递(这里的“值”指的是对象本身)。

Java对象赋值是引用复制还是值拷贝?-图1
(图片来源网络,侵删)

下面我们分三种情况来详细解释,并给出代码示例。


核心概念:引用 vs. 对象

在理解赋值之前,必须先分清两个概念:

  1. 对象:在内存中实际创建的一块数据,new String("Hello") 就会在堆内存中创建一个字符串对象。
  2. 引用:一个变量,它不存放对象本身,而是存放指向该对象在内存中地址的“指针”或“引用”。

你可以把 Person p1 = new Person("Alice"); 想象成:

  • new Person("Alice"):在堆内存中创建了一个 Person 对象。
  • p1:在栈内存中创建了一个引用变量,它指向堆内存中的那个 Person 对象。

直接赋值(最常见,也是最容易出错的)

当你把一个对象引用直接赋值给另一个引用时,两个引用指向了同一个对象

代码示例:

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}
public class Main {
    public static void main(String[] args) {
        // 1. 创建一个 Person 对象,p1 引用指向它
        Person p1 = new Person("Alice");
        System.out.println("p1.name: " + p1.name); // 输出: Alice
        // 2. 将 p1 的引用赋值给 p2
        // p1 和 p2 指向了堆内存中的同一个 Person 对象
        Person p2 = p1;
        // 3. 通过 p2 修改对象的状态
        p2.name = "Bob";
        // 4. 观察 p1 的状态
        System.out.println("p1.name: " + p1.name); // 输出: Bob !!!
        System.out.println("p2.name: " + p2.name); // 输出: Bob
    }
}

输出结果:

p1.name: Alice
p1.name: Bob
p2.name: Bob

修改 p2.name 会影响到 p1.name,因为它们根本就是同一个对象,这在很多情况下会导致意想不到的副作用和程序 bug。


创建新对象进行深拷贝

如果你希望创建一个全新的对象与原对象相同,但修改新对象不会影响原对象,你需要进行深拷贝

深拷贝意味着不仅要复制引用,还要复制引用所指向的对象及其内部的所有对象。

实现深拷贝的方法:

方法 1:手动实现(最常用)

创建一个构造方法,接收另一个对象作为参数,然后逐个复制其成员变量。

class Person {
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 深拷贝构造方法
    public Person(Person other) {
        // 创建新的 String 对象,而不是直接引用
        this.name = new String(other.name); 
        this.age = other.age; // int 是基本类型,直接复制值
    }
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}
public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 30);
        System.out.println("p1: " + p1);
        // 使用深拷贝构造方法创建 p2
        Person p2 = new Person(p1); // p2 是一个全新的对象
        // 修改 p2
        p2.name = "Bob";
        p2.age = 31;
        System.out.println("p1: " + p1); // p1 未被影响
        System.out.println("p2: " + p2); // p2 已被修改
    }
}

输出结果:

p1: Person{name='Alice', age=30}
p1: Person{name='Alice', age=30}
p2: Person{name='Bob', age=31}

注意:

  • 对于基本类型(如 int, double, boolean),直接赋值即可。
  • 对于不可变对象(如 String, Integer, Date),直接赋引用通常是安全的,因为它们的值不能被改变。
  • 对于可变对象(如 ArrayList, HashMap, 自定义的 Person 对象),必须创建新的实例,否则修改嵌套的可变对象会影响原对象。

方法 2:实现 Cloneable 接口(不推荐)

Java 提供了 Object.clone() 方法来实现浅拷贝,要实现深拷贝,你需要重写 clone() 方法并手动复制所有可变对象。

// (Person 类需要实现 Cloneable 接口)
class Person implements Cloneable {
    String name;
    // ... 其他成员
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 1. 调用 super.clone() 得到浅拷贝的对象
        Person cloned = (Person) super.clone();
        // 2. 对可变成员进行深拷贝
        cloned.name = new String(this.name);
        return cloned;
    }
    // ...
}

为什么不推荐?

  • Cloneable 接口的设计有缺陷,它是一个标记接口,没有 clone() 方法。
  • clone() 方法是 protected 的,子类需要处理很多细节。
  • 容易出错,特别是对于复杂的对象图。
  • 通常手动拷贝(如构造方法)或使用序列化更清晰、更安全。

方法 3:通过序列化/反序列化实现(通用但性能较差)

任何实现了 Serializable 接口的对象都可以被序列化成字节流,然后再反序列化回一个新的对象,这个过程天然就是深拷贝。

import java.io.*;
class Person implements Serializable {
    String name;
    // ...
}
public class DeepCopyViaSerialization {
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T deepCopy(T object) {
        try {
            // 1. 序列化到字节流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(object);
            oos.flush();
            // 2. 从字节流反序列化
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("Failed to deep copy object", e);
        }
    }
    public static void main(String[] args) {
        Person p1 = new Person("Alice");
        Person p2 = deepCopy(p1);
        p2.name = "Bob";
        System.out.println(p1.name); // 输出: Alice
    }
}

浅拷贝

浅拷贝会创建一个新的对象,但新对象的成员变量仍然是原对象成员变量的引用,对于基本类型,会复制其值;对于对象引用,会复制引用地址。

如何实现? 最简单的方式就是重写 Object.clone() 方法。

class Address {
    String city;
    public Address(String city) {
        this.city = city;
    }
}
class Person implements Cloneable {
    String name;
    Address address; // Person 对象包含一个 Address 对象
    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
    // 重写 clone() 方法实现浅拷贝
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    @Override
    public String toString() {
        return "Person{name='" + name + "', address=" + address.city + "}";
    }
}
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Address addr = new Address("New York");
        Person p1 = new Person("Alice", addr);
        System.out.println("p1: " + p1);
        // 创建 p2,p2 是 p1 的浅拷贝
        Person p2 = (Person) p1.clone();
        System.out.println("p2: " + p2);
        // 修改 p2 的基本类型成员
        p2.name = "Bob";
        // 修改 p2 的对象成员
        p2.address.city = "London";
        System.out.println("--- After modification ---");
        System.out.println("p1: " + p1); // p1 的 name 未变,但 address.city 被修改了!
        System.out.println("p2: " + p2);
    }
}

输出结果:

p1: Person{name='Alice', address=New York}
p2: Person{name='Alice', address=New York}
--- After modification ---
p1: Person{name='Alice', address=London}
p2: Person{name='Bob', address=London}

p1p2 是两个不同的 Person 对象,但它们内部的 address 引用指向的是同一个 Address 对象,修改 p2.address.city 会影响到 p1


总结与最佳实践

方法 描述 优点 缺点 适用场景
直接赋值 两个引用指向同一个对象。 简单、高效。 修改一个会影响另一个,容易引发 bug。 当你明确希望两个变量操作同一个对象时。
深拷贝 创建一个全新的、内容相同的独立对象。 安全,修改新对象不影响原对象。 实现复杂,性能开销大。 当你需要一个对象的副本,且不希望原对象被意外修改时。
浅拷贝 创建一个新对象,但其成员变量(如果是对象)仍共享引用。 实现简单(只需重写 clone())。 修改嵌套的可变对象会影响原对象。 当对象只包含基本类型或不可变对象时,或者你明确知道共享的风险。

最佳实践建议:

  1. 首选手动拷贝(构造方法):这是最清晰、最不容易出错的方式,通过提供一个接受另一个对象作为参数的构造方法,显式地表达你的意图是创建一个副本。
  2. 谨慎使用 clone():除非你非常清楚 clone() 的工作原理和潜在问题,否则尽量避免使用它,它的行为(浅拷贝)往往不符合直觉。
  3. 序列化拷贝作为备选:对于复杂的、实现了 Serializable 的对象,序列化是一种通用的深拷贝方法,但要注意性能和序列化的开销。
  4. 永远不要混淆“引用”和“对象”:这是理解 Java 对象模型的关键,赋值操作传递的是引用的副本,而不是对象的副本。
分享:
扫描分享到社交APP
上一篇
下一篇