开始之前,我们先来搞懂下面这两个概念:形参&实参。方法的定义可能会用到 参数,参数在程序语言中分为:
- 形参(形式参数) :用于定义函数(方法)时接收的定义参数,不需要有确定的值。
- 实参(实际参数) :用于传递给函数(方法)的参数,必须有确定的值。
在函数里可能会对形参进行修改,那最终会不会对实参有影响?这就涉及到这篇文章讨论的点了。
程序设计语言将实参传递给方法(或函数)的方式分为两种:
- 值传递 (pass by value):其指的是在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。简单来讲,值传递,所传递的是该参数的副本,是复制了一份的,本质上不能认为是一个东西,指向的不是一个内存地址。
- 引用传递(pass by reference) :是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
总结一下值传递和引用传递的区别:
|
| 值传递 | 引用传递 |
| — | — | — |
| 本质 | 创建副本给函数 | 把自己的内存地址给函数 |
| 对实参的影响 | 不会影响实参 | 会影响实参 |
概念说完了,我们先来说结论:java是值传递。也就是说,函数里对形参的修改,不会影响到实参。
那有些人看到这句话,肯定心里嘀咕着你在扯淡吧。举个很简单的例子:
public class ParamTest {
public static void main(String[] args) {
User user = new User();
user.name = "11";
change(user);
System.out.println(user.name);
}
public static void change(User user) {
user.name = "22";
}
}
class User {
public String name;
}
上面的在函数里把user的name改成22,最终打印的也是22,卧槽,如果java是值传递的话,那user对象不应该被修改啊,作者你在胡说八道吧。
看问题看本质,这个问题的本质是什么,是内存的问题,而这个问题里牵扯的是jvm的内存模型里的栈跟堆。栈对于参数的存储是在局部变量表里的,局部变量表存放了编译器可知的八种基本数据类型、对象引用(reference类型,不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
每个线程都分配一个独享的栈,所有线程共享一个堆。对于每个方法的局部变量来说,是绝对无法被其他方法,甚至其他线程的同一方法所访问到的,更遑论修改。
在java里,基本类型(int、double、long 这种)是直接将存储在栈上的,而引用类型则是值存储在堆上,栈上只存储对象的内存地址。
当一个方法里调用另一个含有参数的方法时,会从自己的局部变量表里复制一份相应的值给它,所以就是main方法在局部变量表里把user的对象引用的值拷贝到change方法的局部变量表里,这样的话change可以根据这个引用的值改动了堆里的数据,也会影响到main方法的user。以上面的例子画个图示:
我们把change里的逻辑改一下,把user赋值给新的User:
public class ParamTest {
public static void main(String[] args) {
User user = new User();
user.name = "11";
change(user);
System.out.println(user.name);
}
public static void change(User user) {
user = new User();
user.name = "22";
}
}
class User {
public String name;
}
这么改动后,输出到控制台的name依然是11,为什么呢?看下面的图:
change本来接收到的是拷贝过来的user的对象引用的值0x11,后来user赋值给了新的User,这样的话change里的局部变量里对user的对象引用的值也将会改变成新的**0x12,**这样的话在change里对user的修改都不会影响到main里的user,因为他俩的对象引用的值都不同。
那java为什么是值传递懂了吧!总结一下,java里方法里调用另一个方法的过程中,都不会用到对象的真正的内存地址,而是利用了栈的局部变量表的规则把相关的值保存并复制一份副本传递给其他栈,所以就是值传递!