先明晰一下文中值传递和引用传递的含义(关于对引用的定义的争议请参考评论区)。
值传递:方法调用时,实参把它的值传递给对应的形参(或者说副本),方法执行中形式参数值的改变不影响实际参 数的值。
引用传递:也称为传地址。方法调用时,实参的引用(地址,而不是参数的值)被传递给方法中相对应的形参,在方法执行中,对形参的操作实际上就是对实参的操作,方法执行中形式参数值的改变将会影响实际参数的值。
两个看似没什么区别的代码
代码一
public static void main(String[] args) {
List<Object> objects = null;
foo(objects);
}
public static void foo(List<Object> objectsCopy) {
objectsCopy = new ArrayList<>();
objectsCopy.add(new Object());
// Bla bla bla
}
代码二
public static void main(String[] args) {
List<Object> objects = new ArrayList<>();
foo(objects);
}
public static void foo(List<Object> objectsCopy) {
objectsCopy.add(new Object());
// Bla bla bla
}
真的没有区别吗?
以上是我最近写代码时遇到的,当我使用第一种写法的时候,我发现objects
一直是null,略微诧异了一会,我换了第二种写法,问题解决。
老司机可能一看就知道了,这是一个值传递和引用传递的经典问题。
那么为什么第一个不是引用传递?难道List不是引用类型吗?一图胜过千言万语,先来张图解释一下。
引用本身以及基本数据类型是存放在栈里的,而引用类型所指向的内容存放在堆内。据此,画出了上图所示内容,objects
表示引用本身(堆中的地址,后文地址均表示此意),而content
表示引用指向的具体内容(后文也均使用该词表示引用指向的具体内容)。
解释:
在代码一
中,引用objects
,其值为null
,所以没有指向任何堆内存;
当我们调用foo
方法时,引用objects
首先会在foo
方法中被拷贝一份副本objectsCopy
,objectsCopy
同样也不指向任何堆内容(在foo方法中,所有的操作都是通过objects
的副本来操作的);
当foo
方法中objectsCopy = new ArrayList<>();
语句被调用时,content的地址&content(&在C/C++中表示取地址)就会被写到栈objectsCopy
,此时objectsCopy
就指向了content;
可以很清楚的看到,接下来所有的操作都会改变content的内容,但是很遗憾,objects不会有任何改变,始终为null。
在代码二
中,main函数中执行了语句objects = new ArrayList<>();
,于是引用objects
指向堆中的content
;
当我们调用foo
方法时,引用objects
首先会在foo
方法中被拷贝一份副本objectsCopy
,因为objects
指向堆中的content
,于是objectsCopy
指向堆中的content
;
接下来对objectsCopy
的所有的操作都会改变content的内容,因为objects指向的也是content,所以就改变了objects指向的内容。这就是著名的引用传递。
那么代码一是什么传递?
函数中修改一个存放在栈中的数据,而传递进来的参数是它本身,这是什么传递?或者说函数传了一个引用参数(地址),而现在修改的是引用本身,这是什么传递?
这就是地地道道的、彻头彻尾的goddamn值传递。
仅看表面上传递的是引用类型还是值类型是无法判断这将是值传递还是引用传递,这要取决于你具体的操作是改变引用本身(地址)还是引用指向的内容(content)。
尽管在代码一中,我传递的是一个引用类型,但是我修改的是引用本身,所以它是值传递,它真正操作的部分如上图所示;代码二中我修改的是content,但我传递的是content的引用,所以它才是引用传递。
因此,修改A,传递A的引用,这就是引用传递;修改A,传递A,这就是值传递。传递引用类型不是引用传递。
好绕啊。。。C++或许更好理解一些。
修改引用指向的内容(x和y),传递的是地址(指针)int* x
和int* y
(地址可以对应理解为Java中的引用),这就是引用传递。
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
修改指针,传递的也是指针本身,这就是值传递。
void foo(int* x, int* y)
{
x = new int(2);
y = new int(5);
}
尽管我自认为在C/C++中就已经将这两种传递理解得很透彻了,但是不经意间这错误还是犯得彻彻底底。为此,我总结出这样的一句话:
如果你想修改引用指向的内容,你需要传递引用;如果你想要修改引用本身的值,那么你需要传递引用的引用,否则那只是穿上了引用外衣的值传递。
最后再补充一点C++中值传递和引用传递。
值传递
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
地址传递
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = temp;
}
引用传递
void swap(int &x, int &y)
{
int temp = x;
x = y;
y = temp;
}
除值传递外,地址和引用传递都会改变x和y的值。