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 方法里明明交换了,为什么外部的 xiaoZhangxiaoLi 没有变?

这就是 Java 值传递的本质。

图解真相

让我们用图示来解释发生了什么:

在调用 swap(xiaoZhang, xiaoLi) 之前,内存中大致的结构是:

xiaoZhang  ──→  Person("小张")  在堆中地址 0x001
xiaoLi     ──→  Person("小李")  在堆中地址 0x002

当调用 swap 方法时,形参 person1person2 分别拷贝了 xiaoZhangxiaoLi 中保存的地址值

xiaoZhang  ──→  Person("小张")  0x001  ←──  person1  (拷贝了 0x001)
xiaoLi     ──→  Person("小李")  0x002  ←──  person2  (拷贝了 0x002)

swap 方法中的交换操作,只是交换了 person1person2 这两个拷贝的地址

xiaoZhang  ──→  Person("小张")  0x001  ←──  person2  (现在是 0x001)
xiaoLi     ──→  Person("小李")  0x002  ←──  person1  (现在是 0x002)

方法执行结束后,person1person2 随着栈帧销毁而消失,而 xiaoZhangxiaoLi 指向的地址完全没有变化。

这就是为什么 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 中只有值传递?

posted @ 2026-06-02 17:58  松鼠航  阅读(0)  评论(0)    收藏  举报