点击查看【music163】

前言

关于这个问题,一般存在如下两种错误的理解:

  • Java 是引用传递;
  • 传递的参数是基本数据类型,就是值传递,如果传递的参数是引用类型,就是引用传递。

你的答案是什么?

版本约定

我们都知道,在 Java 中定义方法的时候是可以定义参数的。比如 main 方法:

  1. public static void main(String[] args)

这里面的 args 就是参数。参数在程序语言中分为形式参数和实际参数。

形式参数:简称形参,用于定义方法的时候使用的参数,用来接收调用者传递的参数。形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元,因此,形参只在方法内部有效。

实际参数:简称实参,用于调用方法时传递给方法的参数。实参在传递给方法前是要被先赋值才能传递的。

  1. public class Test4 {
  2. public static void main(String[] args) {
  3. int salary = 1000;
  4. tripleValue(salary);
  5. }
  6. public static void tripleValue(int val) {
  7. val = val * 3;
  8. }
  9. }

比如上面的例子,salary 就是实参,而方法中的 val 就是形参。

值传递与引用传递

上面提到了,当我们调用一个有参数的方法时,会把实参传递给形参。在程序语言中,这个传递过程分为两种类型,即值传递和引用传递。我们来看下程序语言中是如何定义和区分值传递和引用传递的。

值传递(pass by value)是指在调用方法时将实参复制一份传递到方法中,这样当方法对形参进行修改时不会影响到实参。

引用传递(pass by reference)是指在调用方法时将实参的地址直接传递到方法中,那么在方法中对形参所进行的修改,将影响到实参。

Java 的参数传递

根据上面的概念,我们来看看 Java 的参数传递到底是值传递还是引用传递。

先来看一个简单的例子:

  1. public class Test5 {
  2. public static void main(String[] args) {
  3. int i = 1;
  4. System.out.println("Before pass, i = " + i);
  5. pass(i);
  6. System.out.println("After pass, i = " + i);
  7. }
  8. private static void pass(int j) {
  9. j = 2;
  10. }
  11. }

运行程序,输出:

  1. Before pass, i = 1
  2. After pass, i = 1

可见,pass 方法内部对形参 j 的修改并没有改变实际参数 i 的值。根据上面的定义,可以得到结论:Java 的参数传递是值传递。

但是,很快就有人提出了质疑。然后,他们会搬出以下代码:

  1. public class User {
  2. private String name;
  3. private String gender;
  4. public User() {
  5. }
  6. @Override
  7. public String toString() {
  8. return "{" +
  9. "name='" + name + '\'' +
  10. ", gender='" + gender + '\'' +
  11. '}';
  12. }
  13. public String getName() {
  14. return name;
  15. }
  16. public void setName(String name) {
  17. this.name = name;
  18. }
  19. public String getGender() {
  20. return gender;
  21. }
  22. public void setGender(String gender) {
  23. this.gender = gender;
  24. }
  25. }
  26. public class ParamTest {
  27. public static void main(String[] args) {
  28. ParamTest pt = new ParamTest();
  29. User hollis = new User();
  30. hollis.setName("Hollis");
  31. hollis.setGender("Male");
  32. System.out.println("Before pass, user is " + hollis);
  33. pt.pass(hollis);
  34. System.out.println("After pass, user is " + hollis);
  35. }
  36. public void pass(User user) {
  37. user.setName("hollischuang");
  38. }
  39. }

运行程序,输出:

  1. Before pass, user is {name='Hollis', gender='Male'}
  2. After pass, user is {name='hollischuang', gender='Male'}

经过 pass 方法执行后,实参的值(userName)被改变了,根据上面的定义,这不就是引用传递了么。

于是,根据上面的两段代码,有人得出一个新的结论:Java 的方法中,在传递基本数据类型参数的时候是值传递,在传递引用类型参数的时候是引用传递。

但是,这种表述任然是错误的,不信你看下面这个参数类型为对象的参数传递:

  1. public class ParamTest {
  2. public static void main(String[] args) {
  3. ParamTest pt = new ParamTest();
  4. String name = "Hollis";
  5. System.out.println("Before pass, name is " + name);
  6. pt.pass(name);
  7. System.out.println("After pass, name is " + name);
  8. }
  9. public void pass(String name) {
  10. name = "hollischuang";
  11. }
  12. }

运行程序,输出:

  1. Before pass, name is Hollis
  2. After pass, name is Hollis

这又作何解释呢?同样传递了一个对象,但是原始参数的值并没有被修改,难道传递对象又变成值传递了?

上面,我们举了三个例子,表现的结果却不一样,这也是导致很多初学者,甚至很多高级程序员对于 Java 的传递类型有困惑的原因。

其实,我想告诉大家的是,上面的概念没有错,只是代码的例子有问题。来,我再来给大家画一下概念中的重点,然后再举几个真正恰当的例子。

那么,我来给大家总结一下,值传递和引用传递之间的区别的重点是什么。


值传递 引用传递
根本区别 会创建副本(copy) 不创建副本
所以 方法中无法改变原始对象 方法中可以改变原始对象

我们上面看过的几个 pass 的例子中,都只关注了实际参数内容是否有改变。如传递的是 User 对象,我们试着改变它的 name 属性的值,然后检查是否有改变。其实,在实验方法上就错了,当然得到的结论也就有问题了。

为什么说实验方法错了呢?这里我们来举一个形象的例子。再来深入理解一下值传递和引用传递,然后你就知道为啥错了。

你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。

你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。

但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?而我们在 pass 方法中,改变 user 对象的 name 属性的值的时候,不就是在“砸电视”么。你改变的不是那把钥匙,而是钥匙打开的房子。

还拿上面的一个例子来举例,我们真正的改变参数,看看会发生什么?

  1. public class ParamTest {
  2. public static void main(String[] args) {
  3. ParamTest pt = new ParamTest();
  4. User hollis = new User();
  5. hollis.setName("Hollis");
  6. hollis.setGender("Male");
  7. System.out.println("Before pass, user is " + hollis);
  8. pt.pass(hollis);
  9. System.out.println("After pass, user is " + hollis);
  10. }
  11. public void pass(User user) {
  12. user = new User();
  13. user.setName("hollischuang");
  14. user.setGender("Male");
  15. }
  16. }

上面的代码中,我们在 pass 方法中,改变了 user 对象,运行程序,输出:

  1. Before pass, user is {name='Hollis', gender='Male'}
  2. After pass, user is {name='Hollis', gender='Male'}

我们来画一张图,看一下整个过程中发生了什么,然后我再告诉你,为啥 Java 中只有值传递。
23614.png
稍微解释下这张图,当我们在 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,但是实际上并没有。

通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以,上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。

我们再来回顾下之前的那个“砸电视”的例子,看那个例子中的传递过程发生了什么。
23613.png
同样的,在参数传递的过程中,实际参数的地址 0X1213456 被拷贝给了形参,只是,在这个方法中,并没有对形参本身进行修改,而是修改的形参持有的地址中存储的内容。

所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。在判断实参内容有没有受影响的时候,要看传的的是什么,如果你传递的是个地址,那么就看这个地址的变化会不会有影响,而不是看地址指向的对象的变化。就像钥匙和房子的关系。

那么,既然这样,为啥上面同样是传递对象,传递的 String 对象和 User 对象的表现结果不一样呢?我们在 pass方法中使用 name = "hollischuang"; 试着去更改 name 的值,阴差阳错的直接改变了 name 的引用的地址。因为这段代码,会 new 一个 String,再把引用交给 name,即等价于:

  1. name = new String("hollischuang");

参数传递 - 图3
所以说,Java 中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用的地址。

同样的情况还会发生在基本类型对应的包装类型上,根本原因是它们的值都是 final 类型的,不可改变,重新赋值相当于 new 了一个新的对象,所以不会改变原值。

总结

Java 只有值传递,没有引用传递。引用类型的参数本质还是值传递,只不过它传递的是对象的地址副本,这个副本地址也指向同一个对象。

  • 如果参数类型是基本数据类型,那么传过来的就是这个参数的一个副本,在方法中改变了副本的值不会改变原始的值;
  • 如果参数类型是引用类型,那么传过来的就是这个引用参数地址的副本,这个副本存放的是对象的地址。如果在方法中没有改变这个副本的地址,而只是改变地址指向的对象中的内容,那么在方法内的改变会影响到传入的参数;如果在方法中改变了副本的地址,如 new 一个,那么副本就指向了一个新的地址,此时传入的参数还是指向原来的地址,所以不会改变参数的值。

另外注意 String、Byte、Short、Integer、Long、Float、Double、Boolean、Char 的参数传递,在 pass 方法中的修改不会改变原值。

转载

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/mrw8ha 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。