Java中为什么只有值传递?
对于对象参数而言,实际参数传递给形式参数的只是一个内存地址,让形式参数也指向实际参数所指向的地址,传递的值的内容是对象的引用。
为什么会是这样?让我慢慢为你讲解。
对于Java的传递类型,常见错误理解如下:
错误理解一:值传递和引用传递,区分的是传递内容;如何是值,就是值传递;如果是引用,就是引用传递;
错误理解二:Java是引用传递;
错误理解三:传递的参数如果是普通类型,那就是值传递;如果是对象,那就是引用传递。
我们都知道,Java中定义方法的时候是可以定义参数的,比如Java中的main方法,方法中的args就是参数。
public static void main(String[] args)
参数在程序语言中分为实际参数和形式参数。
实际参数由主调函数传入,被调函数接受由主调函数传入的参数(也称为形式参数)。
当我们在程序中调用一个有参函数时,会将实际参数传递给形式参数。这个传递过程分为:值传递和引用传递。
值传递:调用函数传递实际参数时,会将实际参数复制一份副本,将复制的副本传入形式参数,当函数对参数进行修改时,修改的只是副本参数,不会影响到实际参数。
引用传递:调用函数传递实际参数时,会将实际参数的地址传递到形式参数,当函数对参数进行修改时,会影响到实际参数。
值传递 | 引用传递 | |
区别 | 创建副本传入传入 | 将对象地址传入 |
结果 | 函数中无法修改原始变量 | 函数中能够修改原始变量 |
在了解上述概念后,可以写一段代码验证一下,看看Java中到底是值传递?还是引用传递?
在pass方法中修改参数j的值,分别打印出main方法和pass方法中的j值,看看输出结果有什么变化;
最终结果是,pass方法中修改的j值,并没有影响到main方法中的j值。
在接着看如下代码:
在pass方法中修改User对象的值,看看输出结果值:
这次实参的值居然被改变了,根据上述引用传递的定义,实际参数被修改了,这不就是引用传递吗?
先不要着急下结论,在看一个参数类型为对象的参数传递:
输出结果
这又是怎么回事呢?同样传递了一个对象,实际参数却没有被修改,又变成了值传递了?
这里先说一下String类型,当对String类型做任何修改时,都会产生新的String,值也是会变的。
上述的三个例子,表现结果却不一样,这就导致初学者出现困惑,甚至很多高级程序员对值传递都会存在困惑。
再举一个比较形象的例子,在深入理解一下值传递和引用传递
你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。
你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?而我们在pass方法中,改变user对象的name属性的值的时候,不就是在“砸电视”么。你改变的不是那把钥匙,而是钥匙打开的房子。
在拿上面的一个例子进行举例,真正的改变参数,看看会发生什么变化?
输出结果
通过一张图,你就能理解整个过程发生了什么,然后在告诉你,Java中为什么只有值传递?
当在main方法中创建一个User对象时,编译器会在堆中开辟一块内存,保存对象中name和gender的值,然后对象hollis指向该内存的对象0X1234456,如上图所示。
在调用pass方法时,hollis作为实际参数传入到形式参数user,会一并将地址0X123456传递给user,使user也指向地址0X123456,如上图所示。
在pass方法中对参数user进行修改,即重新实例化user,此时user会重新开辟一块内存地址0X45678,使user指向该地址,后面对user的任何修改,都不会在改变地址0X123456中的值,如上图所示。
看到这里,你觉得上述示例是什么传递方式?那肯定不是引用传递,如果是引用传递,肯定会修改实际参数的值,而实际上并没有修改实际参数的值。
这时你在返回去理解一下值传递和引用传递的概念,你就会发现上述的例子实际上是将实际参数的地址传递给了形式参数。
所以上面的传递方式是值传递,并非引用传递,把实际参数的地址当作值传递给形式参数。
在来回顾下这个例子:
看看这个例子中的传递过程发生了哪些变化?
在参数传递过程中,只是把实际参数的地址0X123456传递给了形式参数,方法中没有对形参本身进行修改,而只是修改形参指向地址中的值。
所以,值传递和引用传递的区别并不是在于传递的内容,而是实参到底有没有被复制一份给形参。
在判断实参内容有没有受到影响的时候,要看传递的内容是什么?如果你传递的是地址,就要注意这个地址的变化会不会有影响,而不是看地址指向对象的变化。
讲到这里,我们在反过头来看看String对象,同样都是传递对象,为什么String和User的表现结果不一样呢?
在pass方法中使用name = "hollischuang",试着去更改实际参数name的值,结果直接改变了name的引用地址。
这是因为name = "hollischuang"这段代码,会new一个String对象,再让name指向这个新的地址,等价于下面这段代码:
name = new String("hollischuang");
而实参name还是指向“Hollis”字符串所在的地址,当pass中修改name对象时,并不会修改到实际参数name的值。
所以,这就是为什么Java中只有值传递?只不过对于对象参数,值的内容是对象的引用。
总结
无论是值传递还是引用传递,其实都是一种求值策略(Evaluation strategy)。在求值策略中,还有一种叫做按共享传递(call by sharing)。其实Java中的参数传递严格意义上说应该是按共享传递。
按共享传递,是指在调用函数时,传递给函数的是实参的地址的拷贝(如果实参在栈中,则直接拷贝该值)。
在函数内部对参数进行操作时,需要先拷贝出地址,寻找到具体的值,再进行操作。如果该值在栈中,因为是直接拷贝的值,所以函数内部对参数进行操作不会对外部变量产生影响。
如果原来拷贝的是原值在堆中的地址,那么需要先根据该地址找到堆中对应的位置,再进行操作。因为传递的是地址的拷贝,所以函数内对值的操作对外部变量是可见的。
简单点说,Java中的传递,是值传递,而这个值,实际上是对象的引用。
而按共享传递其实只是按值传递的一个特例罢了。所以我们可以说Java的传递是按共享传递,或者说Java中的传递是值传递。