Java 值引用、参数传递的问题详解

在讨论这个问题之前,先抛出两个概念:值传递和引用传递。学过程序的同学应该对两个概念不陌生,就算忘了也可以看看下面的定义回忆回忆。

值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,在函数中如果对参数进行修改,不会影响到实际参数

引用传递:引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,会影响到实际参数。

虽然有了定义,但是很明显用到实际中还是有些难度,我一直以来就对这个似懂非懂,也没想过把它弄明白,但后面遇到问题时,每次从网上搜索的结果中,五花八门,各种解答都有,有的从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门牌号属于李四”)这时候再通过我家门牌号去查询房地产归属人,肯定还是我的名字,毕竟不管你怎么改,你手里拿的也不是我家门牌号。

 

点个赞吧

 

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值