写于 2016 年 07 月 30 日

  • 一. 值类型和引用类型(此处先不考虑String)的传递
  • 二. String的问题:
      1. String问题来源
    • 2.脑洞猜想可能情况
    • 3.字节码验证
    • 4.最终结论

写这个的原因主要是今天看到了知乎的一个问题,发现自己有些地方有点懵逼,写下来记录一下,知乎上排名第一的答案说的很清楚,不过看了以后依旧有点迷迷糊糊,所以自己写了个几行代码测试。

首先上一个,感觉比较对的结论:

Horstmann的《java核心技术》(中文第8版P115-P117)原文描述:

”java程序设计语言总是采用值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。“

”有些程序员(甚至是本书的作者),认为java程序设计语言对对象采用的是引用调用,实际上这种理解是不对的。”

然后补充几句我的理解:

  1. 首先,Java在传递过程中,传递的只有值,但是表现出来的形式,却既有值传递也有引用传递,因此,没必要纠结于名字,能理解原理即可。
  2. 在传递对象进函数时,对象的所有数据会被拷贝到局部变量中,这也就导致了局部变量修改其成员变量值时会导致原始的变量的成员变量值产生响应的改变,因为他们持有的成员变量的引用指向了同一个地址块(内存空间)。
  3. 而对于传递8种基本变量时,也只是拷贝了值,因此对基本变量其本身的修改,无法导致原始变量的的修改。
  4. 不过这里需要考虑特殊情况,就是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对象。

img

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

img

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

3.字节码验证

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

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

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

img

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

  • 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."<init>":()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引用传递和值传递的理解,有说的不对的,望指正。