值传递与引用传递

面试官问:"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 的指向没有变化。

三、对比总结表 🔴

类型传递内容修改副本修改原始引用修改对象
基本类型值副本❌ 不影响原变量N/AN/A
引用类型引用副本❌ 不影响原引用❌ 不改变原引用指向✅ 可以修改对象
引用类型 + new引用副本❌ 不影响原引用❌ 不改变原引用指向❌ 新对象不影响原引用

四、正确的 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++,对底层有研究。