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

下面我们分三种情况来详细解释,并给出代码示例。
核心概念:引用 vs. 对象
在理解赋值之前,必须先分清两个概念:
- 对象:在内存中实际创建的一块数据,
new String("Hello")就会在堆内存中创建一个字符串对象。 - 引用:一个变量,它不存放对象本身,而是存放指向该对象在内存中地址的“指针”或“引用”。
你可以把 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}
p1 和 p2 是两个不同的 Person 对象,但它们内部的 address 引用指向的是同一个 Address 对象,修改 p2.address.city 会影响到 p1。
总结与最佳实践
| 方法 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 直接赋值 | 两个引用指向同一个对象。 | 简单、高效。 | 修改一个会影响另一个,容易引发 bug。 | 当你明确希望两个变量操作同一个对象时。 |
| 深拷贝 | 创建一个全新的、内容相同的独立对象。 | 安全,修改新对象不影响原对象。 | 实现复杂,性能开销大。 | 当你需要一个对象的副本,且不希望原对象被意外修改时。 |
| 浅拷贝 | 创建一个新对象,但其成员变量(如果是对象)仍共享引用。 | 实现简单(只需重写 clone())。 |
修改嵌套的可变对象会影响原对象。 | 当对象只包含基本类型或不可变对象时,或者你明确知道共享的风险。 |
最佳实践建议:
- 首选手动拷贝(构造方法):这是最清晰、最不容易出错的方式,通过提供一个接受另一个对象作为参数的构造方法,显式地表达你的意图是创建一个副本。
- 谨慎使用
clone():除非你非常清楚clone()的工作原理和潜在问题,否则尽量避免使用它,它的行为(浅拷贝)往往不符合直觉。 - 序列化拷贝作为备选:对于复杂的、实现了
Serializable的对象,序列化是一种通用的深拷贝方法,但要注意性能和序列化的开销。 - 永远不要混淆“引用”和“对象”:这是理解 Java 对象模型的关键,赋值操作传递的是引用的副本,而不是对象的副本。
