一直没有搞清楚Java值传递究竟是什么意思,今天做《剑指Offer》第JZ40题时,给定了一个函数定义如public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2),需要将结果直接存入数组num1num2中。很纳闷,为什么Java值传递的机制,可以将外部传入方法中的参数在方法内修改后,对外部可见呢?于是发誓一定要弄清楚Java值传递的概念,查阅了很多资料,做如下总结。

一、JVM中的堆和栈

要说清值传递,还得先从JVM中的内存模型说起。简单的说一说JVM中的

  • 堆内存:保存Java中的对象与数组(数组其实也是对象)。通过new出来的对象(实体)都保存在堆内存中,当实体失去引用后,该对象内存不会立马被释放,而是由JVM的GC定期回收。
  • 栈内存:保存Java中的局部变量(如定义在方法中的、for循环中声明的循环变量等),当变量离开作用域后,内存会立即被释放,故栈刷新速度很快。

二、Java中的基本数据类型与引用类型

1
2
int num = 10;
String str = "Fan";

如上所示代码中,num是基本数据类型变量,其值10直接保存在变量内部;str是引用数据类型变量,其变量本身保存的是"Fan"在堆内存中的地址,故说str是字符串对象"Fan"的一个引用。

三、为什么是值传递

1. 情况1:数组与基本数据类型

先定义下面两个方法:

1
2
3
4
5
6
7
public void changeArray(int[] num) {
num[0] = 1;
}

public void changeInt(int num) {
num = 1;
}

在方法外部定义如下两个变量并使用方法:

1
2
3
4
5
int[] numArray = {5, 4, 3};
int numInt = 5;

changeArray(numArray);
changeInt(numInt);

得到的结果是numInt依然是5,而numArray变成了{1, 4, 3}。导致这种结果的分析如下:

  • numInt传入changeInt()方法时,将自身(实参)的值拷贝了一份赋给形参num,在方法内部对形参的改变并不影响实参本身。
  • numArray传入changeArray()方法时,将自身(实参)保存的值(也就是指向数组{5, 4, 3}的地址)拷贝了一份赋给形参num,形参持有相同的地址。在方法内部对形参的改变直接作用于其指向的地址上,故实参也会发生改变。

2. 情况2:字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
String str = "Fan";
StringBuffer strBuf = "Fan";

public void changeString(String strChange) {
strChange = "Jin";
}

public void changeStringBuffer(StringBuffer strBuffer) {
strBuffer.append(" Jinpeng");
}

changeString(str);
changeStringBuffer(strBuf);

如上代码执行后的结果是,str依然为"Fan",而strBuf变为了"Fan Jinpeng"。分析:

  • 在方法中,对字符串的重新赋值发生了new操作,即重新new了一个"Jin"对象,并由形参strChange指向该对象地址,此时strChange指向的地址已不再是由实参str拷贝来的地址了,所以不会影响到实参(str)保存的地址。
  • 而对于StringBuffer类型的对象,若在方法内部进行append()操作,形参strBuffer指向的地址没有改变,还是由实参拷贝进来的,所以实参也会发生变化。

3. 情况3:对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
String name;
// Constructer
// setter
}

public void changeName(Person person) {
person.setName("Fan Jinpeng");
}

public void newChangeName(Person person) {
person = new Person();
person.setName("Spider Man");
}

Person fan = new Person();
changeName(fan);
newChangeName(fan);

如上代码执行后的结果是,fan.name被改为了"Fan Jinpeng",而没有第二次被改变成"Spider Man"。分析:

  • changeName()方法将实参fan引用指向的对象地址拷贝了一份给形参person,方法内部的改动直接对该地址的指向,也即指向的对象进行修改(即对堆内存中存储的实体进行修改)。而newChangeName()方法内部将形参重新指向了一个新的Person对象,不再拥有拷贝来的地址,故对形参指向的对象的改动不会影响实参指向的对象。

四、不严谨的说

综上可以用不严谨的话来总结就是,Java是值传递,若方法传入的是对象,在方法内部对对象进行的直接修改是可以对外部发生改变的;然而若方法内部发生了new操作,即改变了形参的地址指向,那么方法内部的操作就是针对新的对象做出的变化,不会影响外部的对象。