值传递与引用传递
面试官问:"Java 是值传递还是引用传递?"
候选人小唐答:"Java 是引用传递。"
面试官写了一段代码:
void swap(String a, String b) {
String temp = a;
a = b;
b = temp;
}
String x = "hello";
String y = "world";
swap(x, y);
System.out.println(x + " " + y); // 输出什么?
小唐说:"world hello。"
面试官:"错了。"
小唐彻底懵了。
【面试官心理】
这道题是 Java 基础中的经典难题。90% 的候选人能背出"Java 是值传递",但真正理解为什么 swap 不生效的只有 50%。能说出"副本"概念的才是真正理解的。
一、Java 永远是值传递 🔴
1.1 核心概念
值传递(Pass by Value):传递的是变量值的副本。
引用传递(Pass by Reference):传递的是变量本身(地址的引用)。
Java 只有值传递,没有引用传递。
void change(int value) {
value = 100; // 修改的是副本,不影响原始变量
}
int num = 10;
change(num);
System.out.println(num); // 仍然是 10
1.2 基本类型的值传递
// 基本类型传递的是值的副本
int a = 10;
change(a); // 传递 a 的副本
void change(int value) {
value = 100; // 修改的是副本
}
// a 的值不变
1.3 引用类型的值传递
这是最容易混淆的地方:
void change(StringBuilder sb) {
sb.append(" world"); // 修改引用指向的对象
}
StringBuilder s = new StringBuilder("hello");
change(s);
System.out.println(s.toString()); // hello world
为什么 s 被修改了?
调用前:
s ──────→ [StringBuilder: "hello"]
调用时(值传递,传递的是引用的副本):
s ──────→ [StringBuilder: "hello"]
sb(副本)也指向同一个对象
change() 中调用 sb.append(" world"):
s ──────→ [StringBuilder: "hello world"]
sb ──────→ [StringBuilder: "hello world"](同一个对象)
调用后:
s ──────→ [StringBuilder: "hello world"]
关键:传递的是引用的副本,但副本指向的对象是同一个。所以通过引用副本修改对象,原始引用也能看到变化。
1.4 最容易错的例子
回到 swap 问题:
void swap(String a, String b) {
String temp = a;
a = b; // 修改的是副本 a,不影响原始变量 x
b = temp; // 修改的是副本 b,不影响原始变量 y
}
String x = "hello";
String y = "world";
swap(x, y);
System.out.println(x + " " + y); // 仍然是 hello world
调用前:
x ──────→ "hello"
y ──────→ "world"
调用时:
x ──────→ "hello"
y ──────→ "world"
a(x的副本) ──────→ "hello"
b(y的副本) ──────→ "world"
swap() 中交换 a 和 b:
a 改为指向 "world"
b 改为指向 "hello"
但 x 和 y 的指向完全没变!
调用后:
x ──────→ "hello"
y ──────→ "world"
⚠️
swap 函数中,交换的是副本(副本指向),而不是原始变量。原始变量的指向完全没有变化。
二、再看一个迷惑性例子 🔴
void change(StringBuilder sb) {
sb = new StringBuilder("new"); // 创建了新对象
}
StringBuilder s = new StringBuilder("hello");
change(s);
System.out.println(s.toString()); // hello
为什么不是 "new"?
调用前:
s ──────→ [StringBuilder: "hello"]
调用时:
s ──────→ [StringBuilder: "hello"]
sb(副本)也指向同一个对象
sb = new StringBuilder("new"):
s ──────→ [StringBuilder: "hello"](原始引用不变)
sb ──────→ [StringBuilder: "new"](副本指向了新对象)
调用后:
s ──────→ [StringBuilder: "hello"](没被修改)
关键:sb = new StringBuilder("new") 修改了副本的指向,但原始引用 s 的指向没有变化。
三、对比总结表 🔴
四、正确的 swap 思路 🔴
4.1 包装一层
// 通过数组包装,模拟引用传递
void swap(String[] arr) {
String temp = arr[0];
arr[0] = arr[1];
arr[1] = temp;
}
String[] pair = new String[]{"hello", "world"};
swap(pair);
System.out.println(pair[0] + " " + pair[1]); // world hello
4.2 使用 AtomicReference
AtomicReference<String> a = new AtomicReference<>("hello");
AtomicReference<String> b = new AtomicReference<>("world");
void swap(AtomicReference<String> x, AtomicReference<String> y) {
String temp = x.get();
x.set(y.get());
y.set(temp);
}
swap(a, b);
System.out.println(a.get() + " " + b.get()); // world hello
五、面试追问链
面试官:"既然是值传递,为什么 sb.append() 能修改 s 的值?"
答案:传递的是引用的副本,副本指向原对象。通过引用副本修改对象内容,原始引用也能看到(因为指向的是同一个对象)。但如果修改引用本身(sb = new ...),原始引用不受影响。
面试官:"Java 有没有办法实现真正的引用传递?"
答案:没有。Java 设计上就是值传递。如果需要"引用传递"的效果,可以使用:
- 包装类(如数组、AtomicReference)
- 返回值
- 单元素数组
{value}
面试官:"C++ 的引用传递和 Java 的值传递有什么区别?"
// C++ 引用传递
void swap(string& a, string& b) {
string temp = a;
a = b; // 直接修改原始变量
b = temp;
}
string x = "hello", y = "world";
swap(x, y);
cout << x << " " << y; // world hello ✅ 真的交换了
C++ 的引用传递:传递的是变量的别名,修改别名直接修改原始变量。
Java 的值传递:传递的是引用的副本,修改副本不影响原始引用。
【面试官心理】
能说出 C++ 引用传递和 Java 值传递区别的候选人,说明对 Java 内存模型有清晰的理解,也侧面证明学过 C/C++,对底层有研究。