Java中到底是值传递还是引用传递?

引言

关于这个问题,很早之前就碰到过,也查过一些资料,但是网上的说法不统一,导致我一直是似懂非懂的状态,结果今天在刷题的时候又遇到了这个问题,于是我决定梳理一下这块内容,把这个坑给填上。
要弄明白这个问题,首先要明确什么是值传递什么是引用传递。
值传递:表示方法接收的是调用者提供的值。
引用传递:表示方法接收的是调用者提供的变量地址。
在Horstman的《JAVA核心技术》上说:“java程序设计语言总是采用值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。”

Java基本类型和引用类型的区别

存储方式

1
2
int num = 10;
String str = "hello";

如图中所示,number属于8种基本类型中的int,所以它的值直接保存在变量中;而str属于引用类型,变量中b保存的是实际对象的地址。一般称这种变量为”引用”,引用指向实际对象,实际对象中保存着内容。

赋值运算的过程

1
2
num = 20;
str = "java";

对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。如上图所示,”hello” 字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)

调用方法传递参数时发生了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//第一个例子:基本类型
void foo(int value) {
value = 100;
}
foo(num); // num 没有被改变
//第二个例子:引用类型,但是没有提供改变自身的方法
void foo(String text) {
text = "windows";
}
foo(str); // str 也没有被改变
//第三个例子:引用类型,提供了改变自身方法
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。
//第四个例子:引用类型,提供了改变自身方法,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。

为什么第三个例子中的sb改变而第四个例子却没有改变呢?
下面是第三个例子的执行过程图示

在执行了builder.append(“4”)之后,可以看出对象是被直接修改了的,所以函数之外的变量也会被修改

而第四个例子中,

在执行builder = new StringBuilder(“ipad”)之后,builder变量被指向了一个新的对象,而原来的对象还是存在的,函数外面的变量也是一直指向原来的对象地址

总结

在Java中,局部变量和方法参数在jvm中的储存方法是相同的,都是在栈上开辟空间来储存的,而对象是存储在堆中的。栈中存储的引用指向堆中的对象,因此只有当我们修改了堆中的对象时,原本函数外指向这个对象的变量才会改变。在C/C++中存在指针,可以使用*p的方式直接访问堆中的对象;而在Java中是不存在指针的,所以要使用点操作符来修改对象。
例如上文第三个例子中 builder.append(“4”) 就是让对象调用append()方法,从而修改builder指向的对象。而上文第四个例子中,执行builder = new StringBuilder(“ipad”)修改的是栈中的引用,根本就没有访问堆中的对象,所以无法对方法外的变量造成影响。
经过上面的分析,我们终于知道引言中Horstman那句话的意思了,Java方法中传入参数都是拷贝,无法被修改。简单来说,就是在以传值的方式传引用,或者说是传值的方式传地址
下面是Java中参数传递的总结:

  • 一个方法不能修改一个基本数据类型的参数
  • 一个方法可以修改一个对象参数的状态
  • 一个方法不能实现让对象参数引用一个新对象

参考资料:
知乎 - Intopass和流浪的小鼠的回答
独恋幽兰的专栏