资讯专栏INFORMATION COLUMN

引用传递和值传递(pass by value vs pass by reference)

longmon / 1053人阅读

摘要:字节码验证于是就写了以下的类,用来验证然后,然后,看字节码如下图。以上,就是整个关于引用传递和值传递的理解,有说的不对的,望指正。

写这个的原因主要是今天看到了知乎的一个问题,发现自己有些地方有点懵逼,写下来记录一下,知乎上排名第一的答案说的很清楚,不过看了以后依旧有点迷迷糊糊,所以自己写了个几行代码测试。
首先上一个,感觉比较对的结论:
**Horstmann的《java核心技术》(中文第8版P115-P117)原文描述:
”java程序设计语言总是采用值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。“
”有些程序员(甚至是本书的作者),认为java程序设计语言对对象采用的是引用调用,实际上这种理解是不对的。”**
然后补充几句我的理解:

首先,Java在传递过程中,传递的只有值,但是表现出来的形式,却既有值传递也有引用传递,因此,没必要纠结于名字,能理解原理即可。

在传递对象进函数时,对象的所有数据会被拷贝到局部变量中,这也就导致了局部变量修改其成员变量值时会导致原始的变量的成员变量值产生响应的改变,因为他们持有的成员变量的引用指向了同一个地址块(内存空间)。

而对于传递8种基本变量时,也只是拷贝了值,因此对基本变量其本身的修改,无法导致原始变量的的修改。

不过这里需要考虑特殊情况,就是String,其表现形式和8种基本变量一样,具体下文有分析,而对于String为何要这么做,我也不清楚,不是很懂 jvm 和 Java 的设计。

一. 值类型和引用类型(此处先不考虑String)的传递:
public class Student {
    int age;
    String name;
}
public class TestReference {
    public static void main(String[] args){
        Student student = new Student();
        student.age = 10;
        System.out.println(student.age);//10
        addAge(student);
        System.out.println(student.age);//11
        addAge(student.age);
        System.out.println(student.age);//11
    }
    
    static public void addAge(Student paramStudent){
        paramStudent.age = 11;
    }
   
    static public void addAge(int paramAge){
        paramAge = 12;
    }
}

对以上代码进行解释

首先addAge(student)调用的是addAge(Student paramStudent),该部分其实很好理解,首先,paramStudet对象,拷贝了传入的studet对象所有的数据,因此paramStudet它所指向的地址,其实和student是一样的,所以,当paramStudent改变它的age值时,其触发的操作和student改变age的值是一样的 ,因为他们都指向了同一个地址块。

其次addAge(student.age)调用的是addAge(int paramAge),也很好理解,paramAge也只是拷贝了studet.age的值,此处为10,然后改变了paramAge的值,但此时paramAge与引用类型不同,它保存的只有一个值,所以其实这个parmaAge作为一个局部变量,并不能对原本的student.age产生任何影响

二. String的问题: 1. String问题来源

上面的例子其实很好搞清楚,但是我在碰到String的时候就有点懵逼了,如果调用以下方法,结果会如注释显示。

public static void main(String[] args){
    Student student = new Student();
    student.age = 10;
    student.name = "dove";
    changeName(student);
    System.out.println(student.name);//dove_2
    changeName(student.name);
    System.out.println(student.name);//dove_2
    changeName2(student.name); 
    System.out.println(student.name);//dove_2         
}      
static void changeName(Student paramStudent){
    paramStudent.name = "dove_2";
}
static void changeName(String paramName){
    paramName = "dove_3";
}
static void changeName2(String paramName){
    paramName += "233";
}

changeName2(String paramName)此处讲道理被调用后应该是"dove_2233",因为String是一个引用类型,也就是说此处的parmaName应该是指向和传入的参数指向了相同的一个地址块,然后对指向的内存进行了修改,然而结果并不是,原因就在于String是一个不可变的类型(为啥不可变呢,具体可以看String类的实现,它是一个final class,并且其内部正真保存着字符串的value[]也是不可变的(final),所以意味着修改Sting是不可能的)。

2.脑洞猜想可能情况

所以猜测上述的changeName2过程类似于

    FuckString fuckString = new FuckString();//paramName
    FuckString fuckString2 = new FuckString(fuckString);//构造出的新的值
    fuckString = fuckString2;//把paramName指向构造出的新值

然后,这就有点想不通了,不可变的类型,String 的 + 是怎么弄的呢?打个断点试试看,强制进入,发现跳转到了StringBuilder的构造方法里,这说明应该是构造了一个新的StringBuilder对象。

同时,底部的Debug里抛出了个错误,说是无法获取StringBuilder.toString(),也就进一步证明此处有新的String的产生。

到这里基本上就验证了我的猜想,String +会产生一个新的String对象,既然这样,反编译下,看下字节码,估计基本就搞定这个懵逼的问题了。

3.字节码验证

于是就写了以下的类,用来验证:

public class Main {
    public static void main(String[] args){
        String s = "dove";
        s += "233";
    }
}

然后javac,然后javap -c,看字节码,如下图。




尝试着解释下该部分代码(不是很看的懂字节码,所以有些解释可能不是很规范,不过讲道理大概意思不会差很远)

String s = "dove";部分字节码及解释

       0: ldc           #2                  // String dove
       2: astore_1

第0行,将一个常量加载到操作数栈,也就是把“dove”这玩意,放进了操作数栈(也不知道是什么东西,蛤蛤)
第2行,将一号数值(下划线1代表一号,大概理解,不是很准确)从操作数栈存储到局部变量表,说白了就是把“dove”给存了起来?

s += "233";部分字节码及解释

       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."":()V
      10: aload_1    
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String 233
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

第3行,这个很明显,不google也知道,new StringBuilder(),也就是搞了个StringBUilder的实例。
第6行,Java虚拟机提供了一些用于直接操作操作数栈,不是很懂,貌似对整体理解影响不大,先过。
第7行,invokespecial 调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法,此处应该是在初始化StringBuilder对象。
第10行,将1号局部变量(下划线1指代一号变量)加载到操作栈,这里应该是指“dove”
第11行,调用对象的实例方法,此处就是调用StringBuilder.append,也就是把“dove”加到了StringBuilder
第14行,将一个常量加载到操作数栈,就是把“233”载入
第16行,调用对象的实例方法,此处就是调用StringBuilder.append,把“233”给加到“StringBuilder”中
第19行,调用对象的实例方法,此处就是调用StringBuilder.toString,而该方法,会触发new String()的操作,因此,会返还一个新的String对象

4.最终结论:

从脑洞断点以及最后的字节码分析可以看出,s +="233",会导致一个新的String对象生成,也就是说,调用changeName2(String paramName)会使得paramName指向一个新的String对象,这样就意味着,对该数据的改变并不会影响本身student.name的值,由此,String懵逼的问题也解决了。

以上,就是整个关于Java引用传递和值传递的理解,有说的不对的,望指正。

发现上次写的时候忘了图片,现在加上。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64995.html

相关文章

  • JS淬炼: Primitive vs. Object

    摘要:值传递引用传递是值传递,是引用传递。但这影响会根据父类是属于还是而有微妙差别。我们设想有一个父类,和两个继承了他的子类和。这时,子类修改该不会影响到父类本身,更不会传递到其他子类上。 Javascript有两种基本数据类型,Primitive和Object。Object是properties的聚合,其property可以是Object也可以是Primitive。Primitive只有v...

    Hancock_Xu 评论0 收藏0
  • [Interview] Pass-by-value vs. Pass-by Reference

    Java is pass-by-value. Pass by value: make a copy in memory of the actual parameters value that is passed in. Pass by reference: pass a copy of the address of the actual parameter. This code will no...

    Lowky 评论0 收藏0
  • 搞定PHP面试 - 深入了解引用

    摘要:引用可以被看作是文件系统中的硬链接。如果具有引用的数组被复制,其值不会解除引用。如果试图这样从函数返回引用,将会报错,因为函数在试图返回一个表达式的结果而不是一个引用的变量。这并不意味着变量内容被销毁了。 1. 什么是引用 在 PHP 中引用是指用不同的名字访问同一个变量内容。PHP 中的变量名和变量内容是不一样的, 因此同样的内容可以有不同的名字。最接近的比喻是 Unix 的文件名和...

    fox_soyoung 评论0 收藏0
  • JavaScript 是传值调用还是传引用调用?

    摘要:所以传递给函数的值是这个值,所以函数执行结束原始变量并不会改变。传值调用在传值调用中,传递给函数参数是函数被调用时所传实参的拷贝。引用类型变量的值是一个指针,指向堆内存中的实际对象。所以传共享调用也可以说是传值调用。 1. 例子 先来看两个个来自于 《JavaScript 高级程序设计》P70-P71 的两个例子。 1.1. 基本类型参数传递 function addTen(num) ...

    darkbug 评论0 收藏0
  • 剖析Laravel队列系统--准备队列作业

    摘要:原文链接我们推送到队列的每个作业都存储在按执行顺序排序的某些存储空间中,该存储位置可以是数据库,存储或像这样的第三方服务。这个数字从开始,在每次运行作业时不断增加。 原文链接https://divinglaravel.com/queue-system/preparing-jobs-for-queue Every job we push to queue is stored in som...

    marek 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<