前言
初学Java的时候,老师在课堂上说“Java有值传递和引用传递”,但网上“Java只有值传递”的呼声很高。
本人在查找资料的过程中,在这两个说法之间反复横跳。经过本人的整理后,其实还真的是Java只有值传递。
什么是值传递?什么是引用传递?
首先,我们先明确一下值传递和引用传递的定义(来自维基百科)。
值传递
When a parameter is passed by value, the caller and callee have two independent variables with the same value. If the callee modifies the parameter variable, the effect is not visible to the caller.
拙译:当一个参数进行值传递的时候,调用者和被调用者有两个值相同的独立变量。如果被调用者修改了参数值,并不会影响调用者。
引用传递
When a parameter is passed by reference, the caller and the callee use the same variable for the parameter. If the callee modifies the parameter variable, the effect is visible to the caller’s variable.
拙译:当一个参数进行引用传递的时候,调用者和被调用者使用同一个变量。如果被调用者修改了参数值,调用者也会受到影响。
总结一下,值传递和引用传递最本质的区别在与,调用者和被调用者有没有使用同一个变量。
可能有人还是对这两个定义抱有疑问,没关系,我们用C++做个例子。
C++中的值传递和引用传递(不感兴趣可略过)
这里先说明下为什么用C++。因为C++的有指针概念,所以对于指针和引用是有做严格区分的,感兴趣的小伙伴可以看下这篇博客:C/C++中的值传递,引用传递,指针传递,指针引用传递。
基于需要,本文对C++的值传递和引用传递来进行简要说明。
C++的值传递
可以看到,a变量(地址为0x22fe4c)在调用f()函数,进行值传递的变量p(地址为0x22fe20)已经是另一个变量了,而且在改变p的值后,地址为0x22fe4c的内容并没有改变,即a的值没有改变。所以值传递无法改变传递的变量的值。
引用传递
可以看到,a变量(地址为0x22fe4c)在调用f()函数,进行值传递的变量p(地址为0x22fe4c)还是同一个变量,而且在改变p的值后,地址为0x22fe4c的内容变为0xff,即a的值发生改变。所以引用传递可以改变传递的变量的值。
总结一下,在C++中的值传递和引用传递在大体上是跟值传递和引用传递的定义相符的。也就是说这个定义是可以在编程语言中适用的。
Java中的值传递和引用传递
说明一下,System.identityHashCode()的作用是用来判断两个对象是否是内存中同一个对象,跟用==判断内存地址是否一样的效果一样,有疑惑的朋友可以看下这篇博客:Java:Object.hashCode()和System.identityHashCode()的区别
值传递
以上可以得到跟值传递定义一样的结论,Java的值传递过程中,会复制传递的参数值到另一个变量,这两个变量之间互不影响,而且只有基本数据类型进行传递时是以值传递的方式。
引用传递
从这个图也可以看出,Java引用传递过程中的两个数组a, b是指向同一个内存地址,这一个变量的改变会影响到另一个变量,而且只有除了String类型以外的其他对象类型在作为参数传递时,是使用引用传递的。
从这两个例子来看,Java既有值传递也有引用传递啊,所以“Java只有值传递”这个说法是错误的?非也。
String类型?
在查找资料的过程中,很多人的说法是“String类型也是值传递”,为什么呢?举个栗子:
public class Test {
public static void main(String[] args){
Test t = new Test();
String s = "oh";
t.test(s);
System.out.println("print in main , ans is " + s);
}
public void test(String s) {
s = "hello";
System.out.println("print in test , ans is " + s);
}
}
运行的结果是这样的:
print in test , ans is hello
print in main , ans is oh
我们可以看到实参s在传入方法test()后,形参s’改变了也不影响实参的值。为什么?(以下解释基于《深入理解Java虚拟机》的理解)
在字符串s传递过程中:
- 虚拟机在常量池划出一块内存,其地址为addr1,存值“oh”;
- 虚拟机在栈中分配s一块内存,内存中存的值为addr1;
- 虚拟机将s复制一份出来,即s‘,s和s’内存不同,但是它们的值都是addr1;
- 将s’传入方法体;
- 方法体在常量池中划分一块内存,其地址为addr2,存值"hello";
- 方法体将s’值改为addr2;
- 方法结束,main打印的是s,因为s存的值为addr1,所以打印出来的结果为addr1存的值:“oh”
所以String类型在调用过程中也是采用值传递。
“Java只有值传递”是错误的吗?
不是,我们拿“Java引用传递”的例子来解释。
public static void main(String[] args) {
int[] a = {1, 2, 3};
f(a);
...
}
public static void f(int[] b) {
b[0] = 100;
...
}
我们在得到结论的时候,是因为:在经过方法f()之后,a[0]的值从1改变为100。可这个过程严格上是引用传递吗?我们从虚拟机内部来观察传递过程:
- 虚拟机在堆上划分了一块用于存储数组a的内存,其内存地址为addr1。
- 虚拟机在栈中分配给a一个内存地址,这个地址存的是addr1。
- 虚拟机复制a,其别名为b,a和b的内存地址不同,但是他们的值都是addr1。
- 将b传入方法,方法改变了addr1的数组的值。
- 方法结束,f和main打印的都是addr1的内存值,都是同一个对象。
在这个过程中,a和b的内存地址不同,但是他们存值的内存地址后的对象是同一个。就是下图的这种关系:
所以,从虚拟机的角度来看,实参a和形参b是两个独立变量,只是实参a把对象地址当做值传递给形参b。按照值传递的定义来看,a和b只是两个值相同的独立变量,Java是值传递。而a和b的值的值(这里不是写错)所指向的内容是同一个,所以我们前面在看到是“引用传递”的情况。
总结
严格来说,Java只有值传递,因为在实参传递的过程中,虚拟机复制了实参的值到形参,并且实参和形参指向的不是同一块内存。这个说法,是基于这种逻辑(a、b、c这三个变量是独立的,b为对象地址):
这时,我们只要保证实参和形参是两个独立的个体,且值都是b就好。
而"Java有值传递和引用传递"这一说法的出现,是因为我们在刚学习Java的时候还不到了解虚拟机的水平,没有了解到,实参和形参是两个值相同的不同独立体,利用这种“美好的”误会来理解“引用传递”吧。
参考资料:
以及正文中提及的文章