1. 由一个问题引出的疑问

今天在看java.io.FileInputStream源码的时候,突然发现Java程序中是可以改变对象的值的,有点违反我以前的认知。因为在我的想法中,局部变量的值的修改是不会影响到原来的值的,但是事实却是被调用函数中参数值的变化导致了原来方法中的值的变化。下面就是一个很简单的例子:

1
2
3
4
5
6
7
8
9
10
public void test() {
int[] i = { 0 };
System.out.println(i[0]);
setValue(i); // 值传递
System.out.println(i[0]);
}

private void setValue(int[] array) {
array[0] = 100;
}

输出结果如下:

0
100

很显然,数组 i 在调用方法 setValue 时进行的值传递,但是为什么 i[0] 的值还是被修改了呢,这里我们需要先了解一下JVM的内存模型。

2. JVM内存模型

Java的虚拟机中内存区域被分为以下几个部分:

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • Java堆
  • 方法区

我们这里需要涉及到的内存区域为Java虚拟机栈Java堆。当一个Java方法被调用时,Java虚拟机栈中就会被压入一个新的栈帧,栈帧中的其中一个部分叫做局部变量区,这个地方专门用于存放方法参数和局部变量。所以结果很明了了,当一个方法被调用时,Java虚拟机栈中被压入一个新的栈帧,而调用这个方法的那个对象(或方法)会把该方法的方法参数压入到栈帧中。这里需要注意的是这里压入的栈帧里面的方法参数是从上一个栈帧(即调用该方法的方法的栈帧)中复制出来的,而其本身并不是上一个栈帧中的值,他只是和上一个栈帧中的对应参数有一样的值而已。

具体过程可以参见下图:
方法调用过程中栈帧的变换

上面的那个栈帧是一个名为addAndPrint的方法的,它在内部调用了一个名为addTwoTypes的栈帧,中间那幅图的下面的栈帧就是在调用方法addTwoTypes时所产生的addTwoTypes的栈帧,可以发现参数1和88.88都是值传递。

通过以上例子我们可以断言:Java中的所有方法方法调用时候的参数引用都是值传递。那么第一节中的那种情况又是为什么呐?

除了Java虚拟机栈,我们还需要了解到Java中的堆内存,Java中的所有的对象都是保存在堆内存中的。如果我们在进行方法调用的时候传入的参数并不是一个纯粹的值(即不是基本的数据类型),而是一个引用类型,那么事情就变得不一样了。因为引用类型是一个指向堆内存中的对应对象的指针,所以在进行方法调用的时候,栈中压入新的栈帧,新栈帧中的引用是从上一个栈帧中复制过来的。但是,因为是从上一个栈帧中复制过来的,所以这个栈帧中的引用类型与前一个栈帧中的对应的引用类型指向的是堆内存中的同一个对象,因为堆内存中的对象只有一份(不过要注意目前有两个引用都指向了这个对象),所以在方法中对该对象进行操作,对象确实被修改了,之后再回到上一个栈帧,对象也还是会处于被修改的状态。所以第一节中的情况是合理的。

3. 总结

以上的情况如果要总结的话,可以用一句话来概括:

Java方法调用时只存在值传递,但是如果传递的参数是对某一个对象的引用,那么对应的对象也会被修改。

下面这两个链接有兴趣的可以再看看,能够加深理解,并且讲解的也很详细

Java规范中关于方法调用有这么一句话(可以在上面链接2的页面中找到):

When the method or constructor is invoked (§15.12), the values of the actual argument expressions initialize newly created parameter variables, each of the declared type, before execution of the body of the method or constructor. The Identifier that appears in the DeclaratorId may be used as a simple name in the body of the method or constructor to refer to the formal parameter.

这里讲的已经很明白了,再结合JVM中的内存模型一起来看,就很清晰了。