这篇文章我会使用简单的类比并且辅以代码示例的方式来阐述方法参数的传值问题。你只需要知道8种基本数据类型以及引用类型在JVM中的存储结构即可。如果你忘记了,没关系,我们先复习一下:
8种基本数据类型分别为byte,short,int,long,float,double,char,boolean。它们在JVM中会直接以数值的形式直接存储于栈(Stack)中,而其他类型为引用类型,类型的实体存储于堆(Heap)中,栈中存储的是堆内存的地址。
现在以幼儿园版本讲一遍:栈相当于每个小朋友背的书包,文具相当于基本类型,可以直接从书包里拿出来,而引用类型相当于家里的钥匙,每个钥匙上都贴着门牌号码,那么有了钥匙就能够找到对应的家(堆)了。
先说结论:参数传递时,实参一定是传值。有人会问:引用类型不是传址么?对的,但是在栈内存的角度看,这个地址仍然是栈中保存的值。因此,正确而且全面的结论是:
在函数的参数传递过程中,形参接收到的实参一定是栈内存中的值的副本。
示例:
例1: 修改基本类型
定义一只铅笔长度为10,shapePencil()方法能将铅笔削短至5:
public class PracticeTest {
public static void main(String[] args) {
int pencilLength = 10;
shapePencil(pencilLength);
System.out.println(pencilLength);
}
// 削笔
private static void shapePencil(int pencilLength) {
pencilLength = 5;
}
}
输出
10
为何不是5?这是因为在shapePencil()中,形参接收到的是pencilLength的一个副本,在函数内实际上在操作另一个对象,与外部对象无关。
换句话说,shapePencil()会将铅笔复制一支并开始削,即使削得再短也和原来那支没关系。
那如何使它有关联呢?削完笔后用复制的笔替换小朋友包里的那支即可:
public class PracticeTest {
public static void main(String[] args) {
int pencilLength = 10;
pencilLength = shapePencil(pencilLength);
System.out.println(pencilLength);
}
// 削笔
private static int shapePencil(int pencilLength) {
pencilLength = 5;
return pencilLength;
}
}
这样就能实现函数内部参数影响外部了。
例2: 修改引用类型
public class PracticeTest {
public static void main(String[] args) {
Person p = new Person("小明");
updatePerson(p);
System.out.println(p.getName());
}
private static void updatePerson(Person person) {
person = new Person("小华");
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
输出
小明
即使使用的是引用类型,在参数传递的过程中只是把传值改为了传递值的引用(栈中保存的地址),并创建一个副本。实际上仍然与外部对象无关。和例1中没有实际区别。
换成操作list如何?仍然不会影响外部。
public class PracticeTest {
public static void main(String[] args) {
Person p1 = new Person("小明");
Person p2 = new Person("小红");
List<Person> personList = new ArrayList<>();
personList.add(p1);
personList.add(p2);
updatePersonList(personList);
System.out.println(personList.size());
}
private static void updatePersonList(List<Person> personList) {
personList = new ArrayList<>();
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在函数内部即使将list置空,外部仍然是有两个元素的list,副本曾经和原元素保存相同的地址值,在置空的那一刻,副本对应的地址发生改变,不影响原元素的指向。
例3: 修改引用类型内部的元素
这个问题重点说明,我们把例2中的例子稍作改动,修改Person中的name,会如何?
public class PracticeTest {
public static void main(String[] args) {
Person p = new Person("小明");
updatePerson(p);
System.out.println(p.getName());
}
private static void updatePerson(Person person) {
person.setName("小华");
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
输出
小华
竟然真的改动了原来对象的内容!为什么?因为对象虽然被复制了,但是复制的是引用。打个比方,你手上有一个家里的钥匙(对象的引用,指向了一个地址),你家代表对象实际存储的位置,那么复制一个钥匙,在上面刻字都不会影响你的钥匙,可是对方使用这个钥匙进入你家,并修改了屋内的各种家具的位置,你回到家的时候是同样能看到这些改动的。
因此我们能衍生出另一个结论:
如果你需要在函数中修改这个对象,请一定用return返回并在调用处进行赋值;如果你需要改动的是对象内部元素,请放心大胆的改。