Java 究竟是值传递仍是引用传递?

关于这个问题,引起过不少普遍的讨论,看来不少程序员对于这个问题的理解都不尽相同,甚至不少人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,可是说不出来为何。程序员

在开始深刻讲解以前,有必要纠正一下你们之前的那些错误见解了。若是你有如下想法,那么你有必要好好阅读本文。函数

错误理解一:值传递和引用传递,区分的条件是传递的内容,若是是个值,就是值传递。若是是个引用,就是引用传递。
错误理解二:Java是引用传递。
错误理解三:传递的参数若是是普通类型,那就是值传递,若是是对象,那就是引用传递。lua

实参与形参

咱们都知道,在Java中定义方法的时候是能够定义参数的。好比Java中的main方法,public static void main(String[] args),这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。spa

形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。
实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。3d

简单举个例子:code

public static void main(String[] args) {
ParamTest pt = new ParamTest();
pt.sout("Hollis");//实际参数为 Hollis
}

public void sout(String name) { //形式参数为 name
System.out.println(name);
}

实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。对象

值传递与引用传递

上面提到了,当咱们调用一个有参函数的时候,会把实际参数传递给形式参数。可是,在程序语言中,这个传递过程当中传递的两种状况,即值传递和引用传递。咱们来看下程序语言中是如何定义和区分值传递和引用传递的。blog

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中若是对参数进行修改,将不会影响到实际参数。
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。内存

有了上面的概念,而后你们就能够写代码实践了,来看看Java中究竟是值传递仍是引用传递 ,因而,最简单的一段代码出来了:字符串

public static void main(String[] args) {
ParamTest pt = new ParamTest();

int i = 10;
pt.pass(10);
System.out.println("print in main , i is " + i);
}

public void pass(int j) {
j = 20;
System.out.println("print in pass , j is " + j);
}

上面的代码中,咱们在pass方法中修改了参数j的值,而后分别在pass方法和main方法中打印参数的值。输出结果以下:

print in pass , j is 20
print in main , i is 10

可见,pass方法内部对name的值的修改并无改变实际参数i的值。那么,按照上面的定义,有人获得结论:Java的方法传递是值传递。

可是,很快就有人提出质疑了(哈哈,因此,不要轻易下结论咯。)。而后,他们会搬出如下代码:

public static void main(String[] args) {
ParamTest pt = new ParamTest();

User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
pt.pass(hollis);
System.out.println("print in main , user is " + hollis);
}

public void pass(User user) {
user.setName("hollischuang");
System.out.println("print in pass , user is " + user);
}

一样是一个pass方法,一样是在pass方法内修改参数的值。输出结果以下:

print in pass , user is User{name='hollischuang', gender='Male'}
print in main , user is User{name='hollischuang', gender='Male'}

通过pass方法执行后,实参的值居然被改变了,那按照上面的引用传递的定义,实际参数的值被改变了,这不就是引用传递了么。因而,根据上面的两段代码,有人得出一个新的结论:Java的方法中,在传递普通类型的时候是值传递,在传递对象类型的时候是引用传递。

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

public static void main(String[] args) {
ParamTest pt = new ParamTest();

String name = "Hollis";
pt.pass(name);
System.out.println("print in main , name is " + name);
}

public void pass(String name) {
name = "hollischuang";
System.out.println("print in pass , name is " + name);
}

上面的代码输出结果为

print in pass , name is hollischuang
print in main , name is Hollis

这又做何解释呢?一样传递了一个对象,可是原始参数的值并无被修改,难道传递对象又变成值传递了?

Java中的值传递

上面,咱们举了三个例子,表现的结果却不同,这也是致使不少初学者,甚至不少高级程序员对于Java的传递类型有困惑的缘由。

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

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中若是对参数进行修改,将不会影响到实际参数。
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

那么,我来给你们总结一下,值传递和引用传递以前的区别的重点是什么。

 

Java 究竟是值传递仍是引用传递?

 

 

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

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

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

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

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

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

public static void main(String[] args) {
ParamTest pt = new ParamTest();

User hollis = new User();
hollis.setName("Hollis");
hollis.setGender("Male");
pt.pass(hollis);
System.out.println("print in main , user is " + hollis);
}

public void pass(User user) {
user = new User();
user.setName("hollischuang");
user.setGender("Male");
System.out.println("print in pass , user is " + user);
}

上面的代码中,咱们在pass方法中,改变了user对象,输出结果以下:

print in pass , user is User{name='hollischuang', gender='Male'}
print in main , user is User{name='Hollis', gender='Male'}

咱们来画一张图,看一下整个过程当中发生了什么,而后我再告诉你,为啥Java中只有值传递。

 

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,可是实际上并无。

经过概念咱们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。因此,上面的参数实际上是值传递,把实参对象引用的地址当作值传递给了形式参数。

咱们再来回顾下以前的那个“砸电视”的例子,看那个例子中的传递过程发生了什么。

 

Java 究竟是值传递仍是引用传递?

 

 

一样的,在参数传递的过程当中,实际参数的地址0X1213456被拷贝给了形参,只是,在这个方法中,并无对形参自己进行修改,而是修改的形参持有的地址中存储的内容。

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

那么,既然这样,为啥上面一样是传递对象,传递的String对象和User对象的表现结果不同呢?咱们在pass方法中使用name = "hollischuang";试着去更改name的值,阴差阳错的直接改变了name的引用的地址。由于这段代码,会new一个String,在把引用交给name,即等价于name = new String("hollischuang");。而原来的那个”Hollis”字符串仍是由实参持有着的,因此,并无修改到实际参数的值。

 

Java 究竟是值传递仍是引用传递?

 

 

因此说,Java中其实仍是值传递的,只不过对于对象参数,值的内容是对象的引用。

总结

不管是值传递仍是引用传递,其实都是一种求值策略(Evaluation strategy)。在求值策略中,还有一种叫作按共享传递(call by sharing)。其实Java中的参数传递严格意义上说应该是按共享传递。

按共享传递,是指在调用函数时,传递给函数的是实参的地址的拷贝(若是实参在栈中,则直接拷贝该值)。在函数内部对参数进行操做时,须要先拷贝的地址寻找到具体的值,再进行操做。若是该值在栈中,那么由于是直接拷贝的值,因此函数内部对参数进行操做不会对外部变量产生影响。若是原来拷贝的是原值在堆中的地址,那么须要先根据该地址找到堆中对应的位置,再进行操做。由于传递的是地址的拷贝因此函数内对值的操做对外部变量是可见的。

简单点说,Java中的传递,是值传递,而这个值,其实是对象的引用。

而按共享传递其实只是按值传递的一个特例罢了。因此咱们能够说Java的传递是按共享传递,或者说Java中的传递是值传递。