1、引用传递
这一次,让你彻底明白Java的值传递和引用传递!_Java后端技术的博客-CSDN博客
值传递: 基本数据类型、String、Object都是值传递(看上面的地址解释的非常详细)
public static void main(String[] args) {
Object o = new Object();
System.out.println(o);
change(o);
System.out.println(o);
}
public static void change(Object o){
o = null;
System.out.println(o);
}
结果:
java.lang.Object@270421f5
null
java.lang.Object@270421f5
基本数据类型的局部变量:存储栈,也就是“虚拟机栈”中。
基本数据类型的成员变量:顾名思义,就是在类体中定义的变量。存储到了堆中
基本数据类型的静态变量:存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失
引用传递:
”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向同一块内存地址,对形参的操作会影响实参的真实内容。
对象,数组等是引用传递。传进来的形参是引用。
定义一个对象的过程:
Person per = new Person();
实际上有两个过程:
Person per; //定义变量
per = new Person(); //赋值
在执行Person per; 时,JVM先在虚拟机栈中的变量表中开辟一块内存存放per变量,在执行per=new Person()时,JVM会创建一个Person类的实例对象并在堆中开辟一块内存存储这个实例,同时把实例的地址值赋值给per变量。因此可见:
对于引用数据类型的对象/数组,变量名存在栈中,变量值存储的是对象的地址,并不是对象的实际内容。
举个例子:
先定义一个对象:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
写个函数测试一下:
public static void PersonCrossTest(Person person) {
System.out.println("传入的person的name:" + person.getName());
person.setName("我是张小龙");
System.out.println("方法内重新赋值后的name:" + person.getName());
}
@Test
//测试
public static void main(String[] args) {
Person p = new Person();
p.setName("我是马化腾");
p.setAge(45);
PersonCrossTest(p);
System.out.println("方法执行后的name:" + p.getName());
}
输出结果:
传入的person的name:我是马化腾
方法内重新赋值后的name:我是张小龙
方法执行后的name:我是张小龙
可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,这印证了上面所说的“引用传递”,对形参的操作,改变了实际对象的内容。
*下面我们对上面的例子稍作修改,加上一行代码:
public static void PersonCrossTest(Person person){
System.out.println("传入的person的name:" + person.getName());
person = new Person();//加多此行代码
person.setName("我是张小龙");
System.out.println("方法内重新赋值后的name:"+person.getName());
}
输出结果:
传入的person的name:我是马化腾
方法内重新赋值后的name:我是张小龙
方法执行后的name:我是马化腾
为什么这次的输出和上次的不一样了呢?
底层逻辑是:我们知道,对象和数组是存储在Java堆区的,而且堆区是共享的,因此程序执行到main()方法中的下列代码时
Person p = new Person();
p.setName("我是马化腾");
p.setAge(45);
PersonCrossTest(p);
JVM会在堆内开辟一块内存,用来存储p对象的所有内容,同时在main()方法所在线程的栈区中创建一个引用p存储堆区中p对象的真实地址,如图:
当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:
person = new Person();
JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。
可以推出:实参也应该指向了新创建的person对象的地址,所以在执行PersonCrossTest()结束之后,最终输出的应该是后面创建的对象内容(张小龙)。
然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。(马化腾)
由此可见:引用传递,在Java中并不存在。
但是有人会疑问:为什么第一个例子中,在方法内修改了形参的内容,会导致原始对象的内容发生改变呢?
这是因为:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
由图可以看出,方法内的形参person和实参p并无实质关联,它只是由p处copy了一份指向对象的地址,此时:
p和person都是指向同一个对象。
因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:
p依旧是指向旧的对象,person指向新对象的地址。
所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系。
结语
因此可见:在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
只是在传递过程中:
如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。
如果是对引用类型的数据进行操作,分两种情况,一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。