前言
关于这个问题,一般存在如下两种错误的理解:
- Java 是引用传递;
- 传递的参数是基本数据类型,就是值传递,如果传递的参数是引用类型,就是引用传递。
版本约定
- JDK 版本:1.8.0_231
- Java SE API Documentation:https://docs.oracle.com/javase/8/docs/api/
正文
形参与实参
我们都知道,在 Java 中定义方法的时候是可以定义参数的。比如 main 方法:
public static void main(String[] args)
这里面的 args 就是参数。参数在程序语言中分为形式参数和实际参数。
形式参数:简称形参,用于定义方法的时候使用的参数,用来接收调用者传递的参数。形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元,因此,形参只在方法内部有效。
实际参数:简称实参,用于调用方法时传递给方法的参数。实参在传递给方法前是要被先赋值才能传递的。
public class Test4 {
public static void main(String[] args) {
int salary = 1000;
tripleValue(salary);
}
public static void tripleValue(int val) {
val = val * 3;
}
}
比如上面的例子,salary 就是实参,而方法中的 val 就是形参。
值传递与引用传递
上面提到了,当我们调用一个有参数的方法时,会把实参传递给形参。在程序语言中,这个传递过程分为两种类型,即值传递和引用传递。我们来看下程序语言中是如何定义和区分值传递和引用传递的。
值传递(pass by value)是指在调用方法时将实参复制一份传递到方法中,这样当方法对形参进行修改时不会影响到实参。
引用传递(pass by reference)是指在调用方法时将实参的地址直接传递到方法中,那么在方法中对形参所进行的修改,将影响到实参。
Java 的参数传递
根据上面的概念,我们来看看 Java 的参数传递到底是值传递还是引用传递。
先来看一个简单的例子:
public class Test5 {
public static void main(String[] args) {
int i = 1;
System.out.println("Before pass, i = " + i);
pass(i);
System.out.println("After pass, i = " + i);
}
private static void pass(int j) {
j = 2;
}
}
运行程序,输出:
Before pass, i = 1
After pass, i = 1
可见,pass 方法内部对形参 j 的修改并没有改变实际参数 i 的值。根据上面的定义,可以得到结论:Java 的参数传递是值传递。
但是,很快就有人提出了质疑。然后,他们会搬出以下代码:
public class User {
private String name;
private String gender;
public User() {
}
@Override
public String toString() {
return "{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
public class ParamTest {
public static void main(String[] args) {
ParamTest pt = new ParamTest();
User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
System.out.println("Before pass, user is " + hollis);
pt.pass(hollis);
System.out.println("After pass, user is " + hollis);
}
public void pass(User user) {
user.setName("hollischuang");
}
}
运行程序,输出:
Before pass, user is {name='Hollis', gender='Male'}
After pass, user is {name='hollischuang', gender='Male'}
经过 pass 方法执行后,实参的值(userName)被改变了,根据上面的定义,这不就是引用传递了么。
于是,根据上面的两段代码,有人得出一个新的结论:Java 的方法中,在传递基本数据类型参数的时候是值传递,在传递引用类型参数的时候是引用传递。
但是,这种表述任然是错误的,不信你看下面这个参数类型为对象的参数传递:
public class ParamTest {
public static void main(String[] args) {
ParamTest pt = new ParamTest();
String name = "Hollis";
System.out.println("Before pass, name is " + name);
pt.pass(name);
System.out.println("After pass, name is " + name);
}
public void pass(String name) {
name = "hollischuang";
}
}
运行程序,输出:
Before pass, name is Hollis
After pass, name is Hollis
这又作何解释呢?同样传递了一个对象,但是原始参数的值并没有被修改,难道传递对象又变成值传递了?
上面,我们举了三个例子,表现的结果却不一样,这也是导致很多初学者,甚至很多高级程序员对于 Java 的传递类型有困惑的原因。
其实,我想告诉大家的是,上面的概念没有错,只是代码的例子有问题。来,我再来给大家画一下概念中的重点,然后再举几个真正恰当的例子。
那么,我来给大家总结一下,值传递和引用传递之间的区别的重点是什么。
值传递 | 引用传递 | |
---|---|---|
根本区别 | 会创建副本(copy) | 不创建副本 |
所以 | 方法中无法改变原始对象 | 方法中可以改变原始对象 |
我们上面看过的几个 pass 的例子中,都只关注了实际参数内容是否有改变。如传递的是 User 对象,我们试着改变它的 name 属性的值,然后检查是否有改变。其实,在实验方法上就错了,当然得到的结论也就有问题了。
为什么说实验方法错了呢?这里我们来举一个形象的例子。再来深入理解一下值传递和引用传递,然后你就知道为啥错了。
你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。
你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。
但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?而我们在 pass 方法中,改变 user 对象的 name 属性的值的时候,不就是在“砸电视”么。你改变的不是那把钥匙,而是钥匙打开的房子。
还拿上面的一个例子来举例,我们真正的改变参数,看看会发生什么?
public class ParamTest {
public static void main(String[] args) {
ParamTest pt = new ParamTest();
User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
System.out.println("Before pass, user is " + hollis);
pt.pass(hollis);
System.out.println("After pass, user is " + hollis);
}
public void pass(User user) {
user = new User();
user.setName("hollischuang");
user.setGender("Male");
}
}
上面的代码中,我们在 pass 方法中,改变了 user 对象,运行程序,输出:
Before pass, user is {name='Hollis', gender='Male'}
After pass, user is {name='Hollis', gender='Male'}
我们来画一张图,看一下整个过程中发生了什么,然后我再告诉你,为啥 Java 中只有值传递。
稍微解释下这张图,当我们在 main 中创建一个 User 对象的时候,在堆中开辟一块内存,其中保存了 name 和 gender 等数据。然后 hollis 持有该内存的地址 0x123456(图1)。
当尝试调用 pass 方法,并且 hollis 作为实际参数传递给形式参数 user 的时候,会把这个地址 0x123456 交给user,这时,user 也指向了这个地址(图2)。
然后在 pass 方法内对参数进行修改的时候,即 user = new User();
,会重新开辟一块 0X456789 的内存,赋值给 user。后面对 user 的任何修改都不会改变内存 0X123456 的内容(图3)。
上面这种传递是什么传递?肯定不是引用传递,如果是引用传递的话,在执行 user = new User();
的时候,实际参数的引用也应该改为指向 0X456789,但是实际上并没有。
通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以,上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。
我们再来回顾下之前的那个“砸电视”的例子,看那个例子中的传递过程发生了什么。
同样的,在参数传递的过程中,实际参数的地址 0X1213456 被拷贝给了形参,只是,在这个方法中,并没有对形参本身进行修改,而是修改的形参持有的地址中存储的内容。
所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。在判断实参内容有没有受影响的时候,要看传的的是什么,如果你传递的是个地址,那么就看这个地址的变化会不会有影响,而不是看地址指向的对象的变化。就像钥匙和房子的关系。
那么,既然这样,为啥上面同样是传递对象,传递的 String 对象和 User 对象的表现结果不一样呢?我们在 pass方法中使用 name = "hollischuang";
试着去更改 name 的值,阴差阳错的直接改变了 name 的引用的地址。因为这段代码,会 new 一个 String,再把引用交给 name,即等价于:
name = new String("hollischuang");
所以说,Java 中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用的地址。
同样的情况还会发生在基本类型对应的包装类型上,根本原因是它们的值都是 final 类型的,不可改变,重新赋值相当于 new 了一个新的对象,所以不会改变原值。
总结
Java 只有值传递,没有引用传递。引用类型的参数本质还是值传递,只不过它传递的是对象的地址副本,这个副本地址也指向同一个对象。
- 如果参数类型是基本数据类型,那么传过来的就是这个参数的一个副本,在方法中改变了副本的值不会改变原始的值;
- 如果参数类型是引用类型,那么传过来的就是这个引用参数地址的副本,这个副本存放的是对象的地址。如果在方法中没有改变这个副本的地址,而只是改变地址指向的对象中的内容,那么在方法内的改变会影响到传入的参数;如果在方法中改变了副本的地址,如 new 一个,那么副本就指向了一个新的地址,此时传入的参数还是指向原来的地址,所以不会改变参数的值。
另外注意 String、Byte、Short、Integer、Long、Float、Double、Boolean、Char 的参数传递,在 pass 方法中的修改不会改变原值。
转载
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/mrw8ha 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。