Java中的值传递和引用传递

网上查了一些资料,有的说Java里面既有引用传递,也有值传递;也有的说Java只有值传递。其实两种说法都是对的,关键是看你怎么理解它们的含义。首先要先搞明白,什么是“值”,什么是“引用”

一般我们认为“值”就是基本的数据类型:整数,浮点,字符和布尔。“引用”则是指向堆内存中对象的一个地址。


首先把“值”和“引用”区分开来理解下

Java中我们通常是这样定义一个变量的: Type variable = ?

“=” 号左边的比较好理解,Type 是一个兼容 “=” 号右边的一种类型,可以是基本类型,也可以是引用类型。variable 是我们定义的变量名。看下面的例子:

1
2
int a = 0;
User user = new User();

上面的int a = 0;,“=”号左边的int a会告诉编译器我们定义了一个int的基本数据类型和名为a的变量,这个变量存在栈中,并且编译器也知道了这是一个基本类型的变量,然后“=”号右边的0也是直接存储在栈中并赋值给变量a。这时候我们可以理解为a的值为0

再看User user = new User();,“=”号左边的User user告诉编译器定义我们定义了一个User的对象(引用数据类型)和名为user的变量,并且编译器知道了这是一个引用类型的变量,然后“=”号右边的new User()会在堆内存中开辟一块空间存储User这个对象,并生成一个唯一的地址值在存储在栈中,最后赋值给变量user。此时user是一个地址值,它指向堆中的User对象,user这个变量它就是堆中的User对象的引用值(不是像0这样的基本数据值直接存在栈中的)。我们把这种连接关系称为“引用”。

讲到这里,大家应该就已经明白,所谓的“引用”和“值”是什么了。简单地说,“值”就是直接在栈中取出来就能访问并且直接修改,而“引用”虽然也是存储在栈中,但显然不能直接访问,需要通过.这种语法来访问并修改堆中的对象各个属性的值。

先来看下基本类型的存取流程

1
2
3
int a = 0;
int b = a;
a = 1;

结果:a = 1, b = 0

上面给变量a赋值为0后,又把b赋值为a,那么问题来了,a是什么?a就是0,所以b就是0。注意:b不是a!这是赋值操作。是把a的值给了b紧接着马上给a赋值为1,这里其实是把a原来为0的值更换成了1,而b是不会受到a的影响的,b的值仍然是0

再来看下引用类型的存取流程(可能比较难理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void change(User user) {
//找到原来的`user`引用指向的对象,并修改name为"Bob"
user.setUsername("Bob");
//将变量`user`赋值为一个新的引用,因为new User()在堆中新开辟了一个空间存储User对象,这个对象的地址值赋值给`user`变量
user = new User();
//找到新的`user`引用指向的对象,并修改name为“Jack”
user.setUsername("Jack");
System.out.println(user.getUsername());
}//到这里结束,变量`user`消失

public static void main(String[] args) {
//在堆中创建一个User对象,将`user`赋值为User对象的引用。
User user = new User();
//找到`user`引用指向的对象,并修改name为"Bob"
user.setUsername("Mike");
System.out.println(user.getUsername());
//将`user`引用拷贝一份到change方法里面作为该方法的实参
change(user);
System.out.println(user.getUsername());
}

上面输出的结果分别为:

1
2
3
Mike 
Jack
Bob

如果能理解上面的注释,说明你已经理解了Java中的值和引用了。

这里再补充一下其它的知识点,change方法里面重新创建的User对象跑去哪里了?在执行完最后一行代码后System.out.println(user.getUsername());,离开了这个方法块(也叫做 作用域),变量user就出栈了,它已经不存在了,而在方法中 new User()出来的这个新的对象还存在堆中,因为这个新的对象已经没有变量指向它了, 所以它会被当成垃圾,会在随后的一段时间里经过垃圾回收器的算法来自动释放掉。
再来看下change方法中第一行代码user.setUsername("Bob");,这里根据传进来的“引用”修改了原来堆中的对象中的属性值,由于方法外部的user和方法内部的’user’指向堆中的同一个对象,所以会看到最后的userName的值为”Bob”,就是所谓的“引用传递”了。

最后我们把“值”和“引用”一起理解为“值”

再来看下有什么不同

1
2
3
4
int a = 0;//a赋值为0
int b = 0;//b赋值为0
a = 1;//a赋值为1
b = a;//b赋值为1

上面的代码很好理解,就是变量的值一直在改变,一直在进行赋值操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void change(int a, int[] arr) {
a = 1;//a赋值为1,这里的a跟外边的a不是同一个a
arr[1] = 10;//根据传入的地址值在堆中找到数组,并修改第2个元素值
}//到这里结束,变量a,arr消失,但堆中的arr数组还存在

public static void main(String[] args) {
int a = 0;//a赋值为0
//在堆中创建一个数组,相当于new int[]{1,2}
int[] arr = {1, 2};
System.out.println("a=" + a + ", arr=" + Arrays.toString(arr));
//拷贝一份a和arr的值,传到change方法里。a的值为0,arr的值假设为"{int[2]@479}"
change(a, arr);
System.out.println("a=" + a + ", arr=" + Arrays.toString(arr));
}

打印结果:

1
2
a=0, arr=[1, 2]
a=0, arr=[1, 10]

为什么a的值没有改变?

我是这么理解的,栈中的数据不是共享的,每块代码块之间的变量和值不会相互影响,并且超过作用域后就会消失。中的数据是共享的。,我们在外面传入了一个arr的“地址值”,里面的代码arr[1] = 10;是根据arr的“地址值”访问并操作了数组里面的元素值,arr这个变量并没有重新赋值,外面在访问这个“地址值”就会看到它的元素值已经被修改。假如里面的代码是arr = {5,6},此时arr变量被重新赋值为一个新的数组了,那么相当就会在堆中创建一个新的数组,然后将arr指向这个新的“地址值”,而并没有访问原来“地址值”所对应的数组。

所以,上边也提到了,访问对象是需要通过.来进行访问和修改的,如user.name,数组元素是通过下标来访问的,如arr[0]。而“=”号是赋值操作,会重新生成一个“值”。这个值要么就是基本数据的值,要么就是用来访问对象或数组的内存值(它们都是存在中),所以我们可以统一理解为“值”。

说了那么多,就看各位怎么理解了,“引用”也是值,但我们习惯叫它“引用”,就是因为它是一个特殊的“值”。

Java中还有一类特殊的引用类型,就是基本类型的包装类,如Integer,Boolean,Character,Float,Long等等,它们虽然也是引用类型,但是却不能直接访问并修改它们的值,只能通过特殊手段来修改,但这样是不规范的。可以看到它们的源码的类和字段都定义了final关键字。有兴趣的小伙伴可以自己动手试试。