参数传递机制
本教程共 100 篇 · 第 37 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
37. 参数传递机制
本节目标:彻底弄清 Java 只有「值传递」——基本类型传的是值副本,引用类型传的是引用地址的副本,从而准确预判方法内改参数会不会影响外面的变量。
「Java 到底是值传递还是引用传递?」这是经典面试题,也是初学者踩得最狠的坑之一。
结论先给出:Java 只有值传递,没有引用传递。
难点在于引用类型传过去后,表现出来的效果容易让人误以为是「引用传递」。这一章就把这层窗户纸捅破。
先说清两个术语
搞混的根源,往往是没弄懂「值传递」和「引用传递」在语言理论里的准确含义。
值传递(pass by value):把实参的值复制一份交给形参。形参是个全新的变量,改它跟实参没关系。
引用传递(pass by reference):把实参变量本身交给形参,两者是同一个东西的两个名字。改形参就是改实参。
关键区别在于:引用传递下,方法能让外面的变量指向另一个对象。这一点 Java 做不到,一次都做不到。
C++ 里的 void f(int& x) 是真正的引用传递,Java 没有对应语法。
基本类型:传的是值的副本
方法拿到的是实参的一个拷贝,改它不影响外面。
public class Main {
static void change(int x) {
x = 100; // 只改了副本
System.out.println("方法内 x = " + x);
}
public static void main(String[] args) {
int a = 10;
change(a);
System.out.println("方法外 a = " + a);
}
}
```text
运行结果:
```text
方法内 x = 100
方法外 a = 10
a 把值 10 复制一份交给 x,从此两人各过各的。x 改成 100,a 依然是 10。
这部分没人会搞错。真正的麻烦从引用类型开始。
引用类型:传的是地址的副本
对象变量存的不是对象本身,而是指向对象的引用(可以理解成地址)。传参时复制的是这个地址,所以形参和实参指向同一个对象。
public class Main {
static void modify(int[] arr) {
arr[0] = 999; // 顺着地址找到那个数组,改它的内容
}
public static void main(String[] args) {
int[] a = {1, 2, 3};
modify(a);
System.out.println(a[0]); // 输出 999,被改了
}
}
```bash
外面的 `a` 看到了变化。很多人到这一步就下结论:「这不就是引用传递吗?」
不是。往下看。
## 反例:交换两个对象引用会失败
这是把问题讲透的关键例子。写一个方法,尝试交换传进来的两个对象:
```java
public class SwapDemo {
static void swap(StringBuilder x, StringBuilder y) {
StringBuilder temp = x;
x = y;
y = temp;
System.out.println("方法内:x = " + x + ", y = " + y);
}
public static void main(String[] args) {
StringBuilder a = new StringBuilder("苹果");
StringBuilder b = new StringBuilder("香蕉");
swap(a, b);
System.out.println("方法外:a = " + a + ", b = " + b);
}
}
运行结果:
方法内:x = 香蕉, y = 苹果
方法外:a = 苹果, b = 香蕉
```bash
方法里换成功了,方法外纹丝不动。
**如果 Java 是引用传递,`x` 就是 `a` 的别名,交换 `x` 和 `y` 必然导致 `a` 和 `b` 也交换。** 结果没有。这一条就足以证伪「引用传递」。
拆开看发生了什么:
- 调用时,`a` 的地址复制一份给 `x`,`b` 的地址复制一份给 `y`。此时四个变量、两个对象。
- 方法内的交换,只是把 `x` 和 `y` 这两个**副本**里存的地址对调了。
- `a` 和 `b` 手里的地址从头到尾没人动过。
- 方法结束,`x` 和 `y` 随栈帧销毁,什么痕迹都没留下。
方法能改「对象里面的内容」,但**永远改不了外面的变量指向谁**。这就是值传递。
## 让副本指向新对象也是白搭
同样的道理,方法里给形参重新赋值,外面完全感知不到:
```java
public class Main {
static void reassign(int[] arr) {
arr = new int[]{9, 9, 9}; // 只让副本指向新数组
arr[0] = 100; // 改的是新数组,跟外面无关
}
public static void main(String[] args) {
int[] a = {1, 2, 3};
reassign(a);
System.out.println(a[0]); // 输出 1,原封不动
}
}
arr 是地址的副本,让它指向别处,外面的 a 毫发无损。
这恰恰说明传的是「值」——地址本身也是一种值。
两种情况的对照
| 场景 | 方法内的操作 | 外面能否看到变化 |
|---|---|---|
| 基本类型 | 给形参赋新值 | 看不到 |
| 引用类型 | 改对象的内容(如 arr[0] = 9) | 能看到 |
| 引用类型 | 给形参赋新对象(如 arr = new int[3]) | 看不到 |
| 引用类型 | 交换两个形参 | 看不到 |
记忆方法很简单:能改「里面」,不能换「指向」。
一句话总结:方法永远拿不到你变量的本体,只拿到一份拷贝。拷贝可以顺着地址去动对象,但拷贝换了目标不关原件的事。
不可变对象的特殊表现
String 传参时的表现常被误解:
public class Main {
static void change(String s) {
s = s + "世界"; // 拼接产生的是新字符串
}
public static void main(String[] args) {
String str = "你好";
change(str);
System.out.println(str); // 输出「你好」,没变
}
}
```bash
看起来像基本类型的行为,实际上仍然是引用的值传递。
原因在于 `String` 不可变——它没有任何方法能修改自身内容。所有「修改」操作都是造一个新对象再让变量指过去,而形参指向哪里外面根本不关心。
包装类 `Integer`、`Long`、`Double` 也是同样的道理,它们全都不可变。所以想通过参数传出一个 `Integer` 结果,注定失败。
> [!WARNING]
> 别把「不可变」和「值传递」混为一谈。String 传参的表现是不可变性造成的,机制上和数组、`StringBuilder` 完全一致,都是引用的值传递。
## 真要带出结果怎么办
既然改不了外面的变量指向,需要方法「输出」多个值时可以这么做。
**方案一:返回值。** 最直接,也最推荐。
```java
static int[] swap(int a, int b) {
return new int[]{b, a}; // 把结果打包返回
}
方案二:改对象内容,而不是换对象。
static void swap(int[] pair) {
int temp = pair[0];
pair[0] = pair[1]; // 改数组内容,外面能看到
pair[1] = temp;
}
```java
调用后 `pair` 里的两个元素确实换了位置,因为动的是对象内部,不是变量指向。
**方案三:返回一个 record。** Java 16 起有更清爽的写法:
```java
public record Pair(int first, int second) { }
static Pair swap(int a, int b) {
return new Pair(b, a);
}
比返回数组可读性好得多,字段有名字,不用记下标。
Tip优先用返回值。靠参数带出结果的写法(俗称「出参」)在 Java 里既别扭又容易出错,只在改动大对象的内部状态时才用得上。
常见疑问
问:既然对象内容能被方法改,说它是引用传递有什么问题?
问题在于用词不准,而且这个不准会导致预判错误。你以为能交换、能重新赋值,实际都不行,代码写出来是错的。术语准确不是抠字眼,是为了让你能正确推断程序行为。
问:那 Java 传对象时到底复制了什么?
只复制那个引用(地址),对象本身一份都没多。所以传一个装了一百万条数据的集合,开销和传一个空对象几乎一样,都是复制几个字节的地址。这也是 Java 传参不用担心性能的原因。
问:怎么在面试里一句话答对这道题?
「Java 只有值传递。基本类型传的是值的副本,引用类型传的是引用值的副本。因为传的是副本,方法内可以通过引用修改对象内容,但无法改变调用方变量的指向——交换两个对象引用的方法必然失败。」
把最后那个反例讲出来,比背结论有说服力得多。