从函数调用栈理解Java值传递

500 阅读3分钟

首先声明,Java函数传参的唯一方式是值传递,没有引用传递。(查了一下,C++中有值传递,引用传递,指针传递)

误解:基础数据类型是值传递,引用类型是引用传递

我将从函数调用栈的角度,解释为啥是值传递,值传递到底怎么传的。

示例代码如下:

package tech.chnjing;

public class PassByValueDemo {
    static void increaseInt(int i) {
        i = i + 1;
    }
    public static void main(String[] args) {
        int i = 0;
        increaseInt(i);
        System.out.println("i 现在的值:" + i);
    }
}

输出结果是

i 现在的值:0

increaseInt函数里面随便你怎么改形参i的值,不会影响外面的实参i的值。为什么呢?看下面的函数调用过程。

  1. 可以看到Main函数中的变量i作为一个原始数据类型int,它的值0是直接分配在栈上的。
  2. 调用increaseInt函数时,会在栈中新建一个栈帧,并在该栈帧的局部变量表中新建一个变量i,将Main栈帧中i的值复制一份给到increaseInt栈帧中的变量i。
  3. 这就是我们说的值传递,把参数的值复制一份传递过去。
  4. 然后执行i = i + 1,此时影响的是increaseInt栈帧中的i的值,变为了1。
  5. increaseInt执行结束,弹出increaseInt栈帧,然后回到Main函数继续执行。我们可以发现Main栈帧在increaseInt执行期间根本没受任何影响。其中的变量i仍然是0。

接下来看看引用类型参数怎么传递的,

package tech.chnjing;

public class PassByValueDemo {
    static class User {
        public User(int age) {
            this.age = age;
        }
        public int age = 20;
    }

    static void changeUserAge(User user) {
        user.age = user.age + 10;
    }

    static void changeUser(User user) {
        user = new User(50); //新user.age为50
    }

    public static void main(String[] args) {
        User user = new User(30);
        changeUser(user);
        System.out.println("user.age 现在的值:" + user.age);
        
        changeUserAge(user);
        System.out.println("changeUserAge 之后 user.age 的值:" + user.age);
    }
}

输出结果:

changeUser 之后 user.age 的值:30
changeUserAge 之后 user.age 的值:40

由结果可知,user所指向的对象并没有被changeUser所改变。说明changeUser中,让形参user指向另一个新User对象,并不会让外面的实参user也指向这个新User对象,而changeUserAge成功改变了user的age属性。

我们先来看changeUser的调用过程。

  1. Main中新建了一个User对象,变量user的值为User对象在堆中的地址。

  2. 进入changeUser函数,创建了一个新的栈帧并在其局部变量表里新建了一个变量user,复制Main栈帧中user变量的值给到changeUser栈帧中的user变量,也就是0xfff0,为User对象在堆中的地址。

  3. 执行user = new User(50);, 此时在changUser栈帧里面的user变量为对象User2的地址,0xffff

  4. 随着changeUser执行结束,changeUser栈帧弹出,回到Main函数,此时Main栈帧中的user变量的值仍然为0xfff0,指向User对象,没有受到任何影响。

可以看出,在changeUser函数中,参数为引用类型,但是和前面例子里的int类型参数一样,仍然是复制了一份user变量的值到新的栈帧里。只不过前面例子中复制的值为0,后面例子中复制的值为0xfff0。

因此我们说Java里面只有值传递,不管你是原始类型还是引用类型。

接下来看changeUserAge的调用过程。

  1. 在执行user.age = user.age + 10;时,通过user变量中的地址0xfff0,找到User对象,将其age属性改为40;

  2. 执行完changeUserAge后,changeUser栈帧弹出,回到Main函数,此时user变量指向的User对象已经被修改过了。

猜想:如果需要,Java怎样实现引用传递来传递参数?

在进入函数调用,创建栈帧时,形参的值设为实参的地址(现在是实参的值)。访问形参的值就会通过实参的地址找到实参,取回实参的值。修改形参指向的对象,就会改变实参指向的对象。