在讨论这个问题之前,先抛出两个概念:值传递和引用传递。学过程序的同学应该对两个概念不陌生,就算忘了也可以看看下面的定义回忆回忆。
值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,在函数中如果对参数进行修改,不会影响到实际参数
引用传递:引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,会影响到实际参数。
虽然有了定义,但是很明显用到实际中还是有些难度,我一直以来就对这个似懂非懂,也没想过把它弄明白,但后面遇到问题时,每次从网上搜索的结果中,五花八门,各种解答都有,有的从JVM底层原理深入浅出地分析,有人拿了很多例子来举例。但我看得多了,反而混淆了。
不过有一个结论是所有讨论分析的前提:将一个对象传递到方法中,传递的是它的地址,至于是否影响值要分情况讨论。所以严格来说,Java中没有所谓的引用传递。
先看一个对象的创建过程,分为三步
Classxx c = new Classxx();
①创建了一个Classxx类型的变量c,c保存在JVM的虚拟机栈(VM stack)中;
②用new关键字创建了一个新的Classxx对象,Classxx对象保存在JVM的堆(Heap)中;
③将堆中的这个对象赋给变量c。
有了这个概念,看下面的代码可能就会简单许多。
Java语言中分为基础类型和对象类型,下面分为三种(注意不是两种)情况讨论
①八种基础类型是通过值传递的方式,也就是说将值复制一份传递给函数
int a = 0;//实参a
void methoda(int a1){//形参a1
a1 = 5;
}
//此时的a的值并没有改变,只是将其值复制并传递给了a1,a1的改变和a没有任何关系
②八种基础类型对应的封装类型和String,它们虽然是对象,但是有其特殊之处。我们看看其源码大概就能理解了
Integer的部分源码
public final class Integer extends Number implements Comparable<Integer>{
...省略
private final int value;
...省略
}
String的部分源码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
...省略
/** The value is used for character storage. */
private final char value[];
...省略
}
不难发现,八大基础类型的封装类型和String类的类和成员属性都使用final关键字修饰过,也就是说其不可继承,创建后也不能更改,那么在传递给函数时,虽然作为对象,传递的肯定也是地址,但要注意的是,这个地址所指向的值可是final修饰后的值,很明显是不可以更改的,所以会在堆中创建一个新的对象来保存这个值,这个过程不就是值传递的复制嘛。
Integer b = 1;//形参,为什么b不用new Integer(1)呢,这是自动拆箱哦
void methodb(Integer b1){//实参
b1 = 10;
}
//虽然b将其原始地址传递给了b1,b1此刻也获得了b的地址
//但当对b1这个地址指向的值进行修改时,是在内存中重新开辟了新的空间来存储10
//然后再将10的地址赋给b1,所以b1中的地址就已经不再是b中的地址了
//也就是说b并没有因为b1修改地址指向的值而被改变
String s = "aaa";
void methodc(String s){
s = "bbb";
}
//原理同上差不多
③其它对象类
可以拿StringBuilder来举例,它并没有使用final修饰,也就是说这个对象是可变的。
StringBuilder sb = new StringBuilder("aaa");//实参
void methodd(StringBuilder sb1){//形参
sb1.append("bbb");
}
//此刻的sb将其地址传递给sb1,sb1通过这个地址向其值尾部添加了bbb
//所以sb就被改变为aaabbb了,这种就符合引用传递的概念
咱们再用StringBuilder来举一个反例
StringBuilder sb = new StringBuilder("aaa");//实参
void methodf(StringBuilder sb1){
sb1 = new StringBuilder("bbb");
}
//sb传递给sb1地址后,两者都指向同一个对象
//然后new StringBuilder("bbb")又在堆中创建了一个新的对象
//此时sb1指向了这个新的对象,所以sb本身并没有被改变,改变的只是sb1所指向的对象
看到这里,不熟悉这个的同学也许会有些混乱,那我们下面自己手动写一个类来测试下,也许理解效果会更好。
给这个类一个name属性、构造方法和get、set方法。
class MyclassTest{
private String name;
public MyclassTest(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
第一个测试
public static void main(String[] args) {
MyclassTest mt = new MyclassTest("张三");//实参mt
method1(mt);
System.out.println(mt.getName());//输出: 李四
}
public static void method1(MyclassTest mt1){//形参
mt1.setName("李四");
}
实参mt存储的是name为“张三”的对象地址,将其复制给mt1,mt1就指向了和mt相同的对象。再调用这个对象的setName()方法将其中的name改变为“李四”,所以输出时,mt的name就被改变了
理解起来就相当于:我(名字叫张三)家有个门牌号,假设可以通过这个门牌号来寻找、修改我家的房地产归属人。现在我将这个门牌号复制一份给你(名字叫李四),你拿着门牌号去相关部门将归属人改成自己的了(比如系统中将“001门牌号属于张三”修改为“001门牌号属于李四”),那么现在再通过我家门牌号去查询房地产归属人,肯定就不是我而你(李四)了。
第二个测试
public static void main(String[] args) {
MyclassTest mt = new MyclassTest("张三");//实参mt
method2(mt);
System.out.println(mt.getName());//输出张三
}
public static void method2(MyclassTest mt1){//形参
mt1 = new MyclassTest("赵六");
mt1.setName("李四");
}
实参mt存储的是name为“张三”的对象地址,将其复制给mt1,mt1就指向了和mt相同的对象。在method2()方法中,我们在堆中创建了一个新的对象,然后将这个对象的也赋给mt1,此时mt1就被替换了。但mt1在被替换前,并没有对原先那个地址指向的对象做任何事,所以mt的地址并没有任何改变。
还是拿门牌号来理解:我(名字叫张三)家有个门牌号,假设可以通过这个门牌号来寻找、修改我家的房地产归属人。现在我将这个门牌号复制一份给你(名字叫李四),你拿在手里啥都没做,然后隔壁邻居家(名字叫赵六)也把他们的门牌号给你,由于你手里只能拿得下一个门牌号,所以你就只保留了邻居家的门牌号,然后你拿着邻居的门牌号去相关部门将归属人改成别人的了(比如系统中将“002门牌号属于赵六”修改为“002门牌号属于李四”)这时候再通过我家门牌号去查询房地产归属人,肯定还是我的名字,毕竟不管你怎么改,你手里拿的也不是我家门牌号。