Java 中实参与形参:为什么 Java 只有值传递?
Java 中实参与形参:为什么 Java 只有值传递?
前言
最近排查一个线上问题时,遇到了一段"看起来没问题,实际有隐藏 bug"的代码:一个方法接收一个 List 参数,在方法内部对集合做了过滤并重新赋值,但调用方拿到的集合却没有任何变化。这背后正是 Java 参数传递机制的问题。本文带你彻底搞清楚 Java 中的实参(实际参数)与形参(形式参数),以及为什么 Java 只有值传递。
什么是实参和形参?
在开始之前,先明确两个基本概念:
- 形参(形式参数):方法定义时在括号里声明的参数变量,它是方法签名的一部分,相当于一个"占位符"。
- 实参(实际参数):调用方法时实际传入的值,它被赋给对应的形参。
举个例子:
// x 和 y 是形参
public int add(int x, int y) {
return x + y;
}
// 3 和 5 是实参
int result = add(3, 5);
参数传递的两种方式
程序设计语言将实参传递给方法的方式通常分为两种:
- 值传递(Pass by Value):方法接收的是实参值的拷贝,会创建一个副本。方法中对形参的修改,不会影响实参。
- 引用传递(Pass by Reference):方法接收的是实参所引用对象在内存中的地址,不会创建副本,对形参的修改将直接影响实参。
很多语言(如 C++、Pascal)同时提供了这两种方式,但 Java 中只有值传递。
这常常引发争议——"Java 不是可以传对象吗?那不就是引用传递?" 别急,我们通过一个经典例子来验证。
经典案例:两个 Person 对象能否交换?
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Person xiaoZhang = new Person("小张");
Person xiaoLi = new Person("小李");
swap(xiaoZhang, xiaoLi);
System.out.println("xiaoZhang: " + xiaoZhang.getName());
System.out.println("xiaoLi: " + xiaoLi.getName());
}
public static void swap(Person person1, Person person2) {
Person temp = person1;
person1 = person2;
person2 = temp;
System.out.println("person1: " + person1.getName());
System.out.println("person2: " + person2.getName());
}
}
输出结果:
person1: 小李
person2: 小张
xiaoZhang: 小张
xiaoLi: 小李
看到结果你会不会觉得疑惑?swap 方法里明明交换了,为什么外部的 xiaoZhang 和 xiaoLi 没有变?
这就是 Java 值传递的本质。
图解真相
让我们用图示来解释发生了什么:
在调用 swap(xiaoZhang, xiaoLi) 之前,内存中大致的结构是:
xiaoZhang ──→ Person("小张") 在堆中地址 0x001
xiaoLi ──→ Person("小李") 在堆中地址 0x002
当调用 swap 方法时,形参 person1 和 person2 分别拷贝了 xiaoZhang 和 xiaoLi 中保存的地址值:
xiaoZhang ──→ Person("小张") 0x001 ←── person1 (拷贝了 0x001)
xiaoLi ──→ Person("小李") 0x002 ←── person2 (拷贝了 0x002)
swap 方法中的交换操作,只是交换了 person1 和 person2 这两个拷贝的地址:
xiaoZhang ──→ Person("小张") 0x001 ←── person2 (现在是 0x001)
xiaoLi ──→ Person("小李") 0x002 ←── person1 (现在是 0x002)
方法执行结束后,person1 和 person2 随着栈帧销毁而消失,而 xiaoZhang 和 xiaoLi 指向的地址完全没有变化。
这就是为什么 Java 中,你永远无法写一个真正交换两个对象引用的方法。
为什么很多人误以为 Java 有引用传递?
因为当我们这样写代码时:
public static void changeName(Person person) {
person.setName("新名字");
}
public static void main(String[] args) {
Person p = new Person("小张");
changeName(p);
System.out.println(p.getName()); // 输出:新名字
}
看起来像是"引用传递",因为 p 的状态确实被修改了。但这不是引用传递,而是因为:
形参
person拷贝了实参p中保存的地址值,两个变量指向了堆中的同一个对象。通过拷贝的地址去修改对象的内容,自然会影响原变量看到的数据。
这和你把房子的地址抄在一张纸条上给别人,别人拿着纸条上的地址找到了同一栋房子并把门漆成了红色,你回去当然也能看到红色的大门了。但如果别人把纸条上的地址改成了另一个房子,你那边的地址并不会变。
一句话总结:你可以通过形参去修改对象的内容,但你没法通过形参让实参指向另一个对象。
实际开发中的坑
回到文章开头提到的线上问题场景。简化的代码如下:
// 调用方
public void submit(List<OrderSku> orderSkus) {
// 过滤掉不符合要求的 SKU
filterSkus(orderSkus);
// 这里继续用 orderSkus 做后续逻辑
// 期望此时 orderSkus 已经是过滤后的集合了 —— 但实际上没有!
doSomething(orderSkus);
}
// 过滤方法
public void filterSkus(List<OrderSku> orderSkus) {
// 创建了一个新的过滤后集合
orderSkus = orderSkus.stream()
.filter(sku -> sku.isValid())
.collect(Collectors.toList());
// 形参指向了新集合,但实参不受影响!
}
filterSkus 方法中的 orderSkus = ... 只是让形参指向了一个新的 List 对象,调用方的 orderSkus 变量依然指向原来的集合。
正确的修复方式
有两种方式可以修复这类问题:
方式一:使用返回值(推荐)
public List<OrderSku> filterSkus(List<OrderSku> orderSkus) {
return orderSkus.stream()
.filter(sku -> sku.isValid())
.collect(Collectors.toList());
}
// 调用方
orderSkus = filterSkus(orderSkus);
方式二:直接修改原集合的内容
public void filterSkus(List<OrderSku> orderSkus) {
// 使用迭代器移除不符合要求的元素
Iterator<OrderSku> it = orderSkus.iterator();
while (it.hasNext()) {
if (!it.next().isValid()) {
it.remove();
}
}
}
总结
| 特性 | 说明 |
|---|---|
| Java 的参数传递方式 | 只有值传递 |
| 基本类型参数 | 拷贝的是数值本身 |
| 引用类型参数 | 拷贝的是对象的地址值(引用) |
| 能修改对象内容吗? | 可以——通过拷贝的地址访问同一个对象 |
| 能让实参指向新对象吗? | 不能——形参只是实参地址的拷贝 |
理解了这个机制,在写代码时记住一条简单的原则:如果你需要让方法改变某个引用变量指向的对象,那就把新对象通过返回值传出来,而不是依赖参数传递。
参考资料:为什么说 Java 中只有值传递?

浙公网安备 33010602011771号