在Java面试和日常编码中,一个高频疑惑是:为什么把对象传给方法后,方法里改了对象的字段,外面的对象也跟着变;可要是方法里让参数指向别的对象,外面的引用却没变。这背后正是Java只有值传递这一机制在起作用。要真正理解它,不能只看现象,得从内存模型和栈帧复制规则入手。

一、值传递与引用传递的概念厘清
很多初学者把引用传递理解成把对象本身传进去,其实在计算机语言里,传递方式描述的是实参如何交给形参。值传递指的是把实参的值复制一份给形参,形参拿到的是副本;引用传递指的是形参和实参绑定同一块内存,改形参就是改实参。Java明确选择了前者,不管是基本类型还是对象类型,统一走值传递。
对于基本类型,比如int a = 5,传递时把5复制过去,方法里改了副本不影响原变量。对于引用类型,比如Object obj,变量obj本身存的是堆中对象的地址,传递时复制的是这个地址值。所以形参和实参指向同一个对象,但两个变量是独立的,各自存了一份相同的地址。这个区别是理解本质的关键。
二、从内存模型看方法调用
当调用一个方法时,JVM会创建新的栈帧,里面包含局部变量表。实参的值被计算后,按顺序放入形参对应的局部变量槽。如果是基本类型,槽里放的就是数值本身;如果是引用类型,槽里放的是引用值,也就是对象头的地址。整个过程没有把对象搬进栈,也没有让两个变量共享同一个变量存储位置。
我们可以用一段代码观察现象。下面例子中,changeRef试图让参数指向新对象,modifyField修改对象字段:
public class PassTest {
static class User {
String name;
}
static void changeRef(User u) {
u = new User(); // 只改了副本里的地址
u.name = "inner";
}
static void modifyField(User u) {
u.name = "changed"; // 通过副本地址找到原对象改属性
}
public static void main(String[] args) {
User user = new User();
user.name = "origin";
changeRef(user);
System.out.println(user.name); // 输出 origin
modifyField(user);
System.out.println(user.name); // 输出 changed
}
}
运行结果说明,changeRef里u = new User()只是把形参u这个副本指向了新对象,main里的user依旧指向原来的User。而modifyField通过副本里的地址访问到原对象,所以字段变化对外可见。这证明传递的是地址值,不是对象,也不是引用本身。
三、为什么容易误以为是引用传递
误区常来自对引用一词的混淆。在C++里,引用传递是用&声明的别名机制,形参就是实参的别名,改形参必定改实参。Java没有这种语法,它的引用类型变量只是类似指针的对象句柄。由于方法内能通过参数改对象状态,初学者就觉得对象被引用传进去了。
另一个误导是觉得Java对象传参和C里传指针效果一样,于是套用指针传递的说法。但即便C传指针也是值传递指针变量,Java的引用变量传递与之类似,只是语言层隐藏了星号。只要记住:任何Java方法调用,实参变量里的东西被复制给了形参变量,就是值传递。
四、用对比表看清差异
把常见说法和实际行为列出来,能更快建立正确认知:
| 操作 | 基本类型参数 | 引用类型参数 |
|---|---|---|
| 方法内改参数变量本身 | 不影响调用方 | 不影响调用方 |
| 方法内改参数指向对象的字段 | 无对象概念 | 影响调用方原对象 |
| 传递时复制的内容 | 数值 | 堆地址值 |
表里可见,无论哪种类型,方法内对参数变量重赋值都不会回流到调用方,这正是值传递的铁证。引用类型之所以能改外部状态,是因为副本和原变量存了相同地址,而非因为传递方式特殊。
五、实践中的注意点
写工具方法时,如果想让调用方拿到新对象,应该把新对象return出来,而不是指望改参数引用。例如解析配置后生成新实例,用return Config parse(...)比传一个空对象进去填值更清晰,也避免误解。对于集合类,方法里clear或add会影响外面,但list = new ArrayList()不会替换外面的list。
多线程场景下更要小心。如果多个方法持有同一对象的引用副本,任一处通过副本改了共享对象,其他线程看到的都是变动后状态。理解值传递只解决变量绑定问题,不解决对象共享带来的并发安全,该加锁还得加锁。把传递机制和对象可见性分开想,代码才不容易出 bug。
六、小结
Java只有值传递,本质是方法调用时把实参的变量内容复制到形参。基本类型复制数据,引用类型复制地址。因为地址相同,方法内可通过它改对象,但重绑形参不影响外界。抓住栈帧复制和变量独立这两个点,就能彻底明白为什么Java不存在引用传递,也能在写代码时避开参数误用的坑。