值传递与引用
Java 方法参数传递只有一种方式:值传递。
这句话非常重要,也非常容易被误解。很多人看到方法里能修改对象字段,就以为 Java 有引用传递。实际上 Java 传递的是“变量值的副本”。对于基本类型,这个值就是具体数值;对于引用类型,这个值是对象引用的副本。
# 1. 什么是值传递
值传递指调用方法时,把实参变量中保存的值复制一份给形参。
public void change(int value) {
value = 20;
}
int number = 10;
change(number);
System.out.println(number); // 10
过程:
调用前:
number = 10
调用 change(number):
把 number 的值 10 复制给形参 value
方法内:
value = 20
方法结束:
number 仍然是 10
形参 value 是一个新的局部变量,修改它不会影响外部变量 number。
# 2. 基本类型参数
public static void add(int x) {
x++;
}
public static void main(String[] args) {
int a = 1;
add(a);
System.out.println(a); // 1
}
栈帧关系:
main 栈帧
┌────────┐
│ a = 1 │
└────────┘
│ 复制值 1
▼
add 栈帧
┌────────┐
│ x = 1 │ -> x++ 后 x = 2
└────────┘
add 返回后,main.a 仍然是 1
# 3. 引用类型参数
引用类型变量保存的是对象引用。传参时,复制的是引用值。
public static void rename(User user) {
user.setName("Jerry");
}
User u = new User("Tom");
rename(u);
System.out.println(u.getName()); // Jerry
过程:
调用前:
u ─────────────▶ User{name="Tom"}
调用 rename(u):
把 u 中保存的引用值复制给形参 user
方法内:
u ─────────────▶ User{name="Tom"}
user ──────────┘
执行 user.setName("Jerry"):
u ─────────────▶ User{name="Jerry"}
user ──────────┘
外部对象变了,不是因为引用本身被传递过去了,而是因为两个引用副本指向同一个对象。
# 4. 重新赋值引用参数
如果在方法中让形参指向新对象,不会影响外部引用。
public static void reset(User user) {
user = new User("Alice");
}
User u = new User("Tom");
reset(u);
System.out.println(u.getName()); // Tom
过程:
调用前:
u ─────────────▶ User{name="Tom"}
进入方法:
u ─────────────▶ User{name="Tom"}
user ──────────┘
方法内重新赋值:
u ─────────────▶ User{name="Tom"}
user ─────────▶ User{name="Alice"}
方法结束:
user 消失,u 仍指向原对象
这说明 Java 不是引用传递。如果是引用传递,user = new User("Alice") 会让外部 u 也指向新对象。
# 5. swap 经典面试题
# 5.1 交换基本类型
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int x = 1;
int y = 2;
swap(x, y);
System.out.println(x); // 1
System.out.println(y); // 2
方法里交换的是副本。
main: x = 1, y = 2
│ │
▼ ▼
swap: a = 1, b = 2
swap 内交换后:
swap: a = 2, b = 1
main 不变:
main: x = 1, y = 2
# 5.2 交换对象引用
public static void swap(User a, User b) {
User temp = a;
a = b;
b = temp;
}
User u1 = new User("A");
User u2 = new User("B");
swap(u1, u2);
System.out.println(u1.getName()); // A
System.out.println(u2.getName()); // B
过程:
调用前:
u1 ─────▶ User("A")
u2 ─────▶ User("B")
进入方法:
u1 ─────▶ User("A") ◀──── a
u2 ─────▶ User("B") ◀──── b
swap 内交换 a 和 b:
u1 ─────▶ User("A") ◀──── b
u2 ─────▶ User("B") ◀──── a
方法结束:
a、b 消失,u1、u2 不变
方法无法交换外部两个引用变量本身。
# 6. 修改对象字段为什么生效
public static void change(User user) {
user.setName("Changed");
}
这里修改的是对象内部状态:
外部引用 u
│
├──────────────┐
▼ ▼
同一个 User 对象 形参引用 user
两个引用副本指向同一个对象,因此通过任意一个引用修改对象字段,其他引用看到的对象状态也会变化。
# 7. 数组参数
数组也是对象。
public static void update(int[] arr) {
arr[0] = 100;
}
int[] numbers = {1, 2, 3};
update(numbers);
System.out.println(numbers[0]); // 100
数组元素被修改,是因为形参和实参指向同一个数组对象。
但重新赋值数组形参无效:
public static void reset(int[] arr) {
arr = new int[] {9, 9, 9};
}
int[] numbers = {1, 2, 3};
reset(numbers);
System.out.println(Arrays.toString(numbers)); // [1, 2, 3]
# 8. String 参数
String 是引用类型,但不可变。
public static void append(String text) {
text = text + "!";
}
String s = "hello";
append(s);
System.out.println(s); // hello
过程:
s ───────▶ "hello"
text ────┘
text = text + "!"
s ───────▶ "hello"
text ───▶ "hello!"
方法中只是让形参 text 指向新字符串,不会影响外部变量 s。
如果需要返回新值,应显式返回:
public static String append(String text) {
return text + "!";
}
s = append(s);
# 9. final 参数
final 修饰参数,表示形参变量不能重新赋值。
public void update(final User user) {
user.setName("Tom"); // 可以
// user = new User("Jerry"); // 编译失败
}
final 限制的是参数变量本身,不限制对象内部状态。
final user ─────▶ User 对象
│
└─ 不能让 user 改指向
User 对象内部字段是否能改,取决于对象自身是否可变
# 10. 如何让方法影响外部结果
# 10.1 返回新值
public static int add(int value) {
return value + 1;
}
number = add(number);
# 10.2 修改可变对象
public static void addName(List<String> names) {
names.add("Tom");
}
# 10.3 使用包装结果对象
public class Result {
private int value;
}
# 10.4 使用 Atomic 类
public static void increment(AtomicInteger value) {
value.incrementAndGet();
}
但不要为了绕过值传递而滥用可变容器。最清晰的方式通常是返回结果。
# 11. 方法调用中的对象生命周期
public User create() {
User user = new User("Tom");
return user;
}
user 局部变量会随着方法栈帧销毁,但对象不一定销毁。
create 栈帧
┌──────────────┐
│ user 引用 │
└──────┬───────┘
▼
堆内存 User 对象
return user 后:
调用方拿到引用
局部变量 user 消失
堆中对象继续存活
只要对象仍然可达,就不会被 GC 回收。
# 12. 常见误区
# 12.1 “对象参数是引用传递”
不准确。对象参数传递的是引用值的副本。
# 12.2 “方法里修改对象,说明不是值传递”
不准确。修改对象内容和修改外部变量引用是两回事。
# 12.3 “final 对象不可变”
不准确。final 引用不能改指向,对象是否可变取决于类设计。
# 12.4 “String 传进去改不了是因为 String 特殊传值”
不准确。String 也是引用值传递,只是它不可变,拼接会创建新对象。
# 13. 判断题训练
| 代码行为 | 外部变量是否变化 | 原因 |
|---|---|---|
方法内 x = 2,x 是 int 参数 | 不变 | 改的是副本 |
方法内 user.setName("A") | 对象状态变化 | 同指向一个对象 |
方法内 user = new User() | 外部引用不变 | 改的是形参副本 |
方法内 arr[0] = 1 | 数组元素变化 | 同指向一个数组 |
方法内 arr = new int[10] | 外部数组不变 | 改的是形参副本 |
方法内 str = str + "x" | 外部字符串引用不变 | 新字符串赋给形参 |
# Tips 快问快答
Q:Java 到底是值传递还是引用传递? A:只有值传递。基本类型传数值副本,引用类型传引用值副本。
Q:为什么方法里能修改对象字段? A:形参引用副本和外部引用指向同一个对象,修改的是对象内部状态。
Q:为什么方法里 user = new User() 不影响外部变量?
A:这只是让形参副本指向新对象,外部引用变量没有被修改。
Q:Java 能写一个通用 swap 方法交换两个变量吗? A:不能直接交换外部两个局部变量,因为方法拿到的只是值副本。
Q:数组作为参数为什么能被修改? A:数组是对象,传入的是数组引用值副本,两个引用指向同一个数组对象。
Q:String 为什么传进方法后改不了?
A:String 不可变,拼接会生成新对象,并只改变方法内形参的指向。
Q:final 参数有什么用?
A:防止方法内部重新给参数变量赋值,有助于表达该引用不应改指向。
Q:想让方法改变外部值,推荐怎么做? A:优先返回新值;需要共享状态时再修改可变对象。
Q:对象作为返回值时,方法结束对象会消失吗? A:不会。局部变量消失,但返回的对象只要仍可达,就会继续存活。
Q:面试中如何一句话解释? A:Java 永远是值传递;对象参数传的是引用这个值的副本,所以能改对象内容,不能改外部引用变量本身。