1.引子
对于初学者而言,方法的参数传递是个不好理解的东西。对于不少行业老司机,也不一定能完全说得清楚。有的人说,基本数据类型参数做值传递,引用数据类型做地址传递;还有的人说,它们都是做地址传递…。这些说法可以认为是正确的,但也不完全正确。很多说法都没有接触到实质,从计算机原理来说,不论何种数据类型,它们的参数传递方式是一致的,都是传递“指针的副本”。
2.基本数据类型参数传递
对于基本数据类型而言,它本身只有“值”,没有方法、属性。当其做局部变量时,它直接存在栈中。系统自动分配一个栈空间,去储存它的“值”,其实这个“值”可以看做一个“地址”,对于基本数据类型而言,将这个“地址”当做值来用了而已(在JVM内部实现原理上有明显的体现,如JVM的基本模型0op-Klass,这里的Oop不是指面向对象编程,而是指Orindal Object Pointer普通对象指针,为什么这里的指针要加“普通”二字,就是因为引用数据类型的句柄是“一般情况下的指针”,而基本数据类型是将“指针”或者说“地址”来当作“值”使用了,这可以说是一种“特殊的指针”)。可以这么认为此时“值”=“储存的地址值”(注意这里的“储存的地址值”不是这个基本数据类型变量本身的地址)。在方法调用时,方法得到的是参数值的副本,方法不能改变传递给它的参数变量的内容。
- 基本数据类型在栈(Stack)内存中的分布
- 参数传递示意图
//定义基本数据类型的变量的自增运算方法
public static int inc(int num)
{
num = num + 1;
return num;
}
public static void main(String[] args)
{
int a = 4;
System.out.println("变量a的原始值是:" + a); //4
System.out.println("变量a作为参数进行方法调用后,方法的返回值:" + inc(a)); //5
System.out.println("变量a作为参数进行运算后,a本身的值是:" + a); //只改变其副本的值,a仍为4
}
- 输出结果
3.引用数据类型参数传递
对于引用数据类型而言,我们一般要通过它的引用去访问、更改其属性或调用方法(匿名对象除外,即类似于new T().name或new T().setName()的形式)。一般情况下用"T t = new T( )"的形式声明并创建对象,“new T()”创建的对象被系统分配在堆(Heap)内存空间中;而此对象的引用t(或称为“句柄”)被分配在栈(stack)内存空间中,它的值是一个指向刚创建的对象的在堆内存中的首地址。
- 引用数据类型的内存分布
(1)引用数据类型参数可影响对象状态
当引用数据类型变量作为方法的参数时,方法得到的是这个对象引用的副本,因为对象的引用与这个引用的副本同时引用同一个对象,所以方法体内的一些操作会直接影响这个对象的内容或状态。
- 示意图1
//定义一个给学生名字添加统一前缀的方法
public static void formatName(Student stu)
{
if (!stu.getName().startsWith("stu_"))
{
stu.setName("stu_" + stu.getName());
}
}
public static void main(String[] args)
{
Student s1=new Student("王晓莉","女");
Student s2=new Student("李天恒","男");
System.out.println("s1同学原来的名字:"+s1.getName());
formatName(s1); //此方法得到的是句柄s1的副本
/*
* 对象的引用及这个引用的副本同时引用同一个对象,
* 所以方法内的一些操作会直接影响这个对象的内容或状态
* s1的名字将被改为stu_王晓莉
*/
System.out.println("方法调用后s1同学的名字:"+s1.getName());
}
- 输出结果
(2)引用类型参数不能改变引用关系
也是因为方法得到的参数是对象引用的副本,而不是对象引用本身,所以在调用方法后这个对象引用不会被更改为去引用另一个对象。
- 示意图2
//定义一个试图更改句柄指向的方法
public static void swap(Student stu1, Student stu2)
{
Student tmp = stu1;
stu1 = stu2;
stu2 = tmp;
System.out.println("在方法体内对象引用互换后,s1对应的对象引用副本stu1的对应学生名字:"+stu1.getName());
System.out.println("在方法体内对象引用互换后,s2对应的对象引用副本stu2的对应学生名字:"+stu2.getName());
}
public static void main(String[] args)
{
Student s1=new Student("王晓莉","女");
Student s2=new Student("李天恒","男");
swap(s1,s2);//方法体代码执行时,只用到了s1 s2这两个对象引用的副本stu1 stu2
/*
*方法调用后,对象引用的副本被交换了,而对象引用本身没有被交换
*/
System.out.println("在调用方法 swap(s1,s2)后,s1对象引用本身的对应学生名字:"+s1.getName());
System.out.println("在调用方法 swap(s1,s2)后,s2对象引用本身的对应学生名字:"+s2.getName());
}
- 结果输出
4.总结
从以上试验结果来看,方法的参数传递方式都是传递“指针的副本”,或者说是“值传递”。