最近在看Java核心技术的时候,遇到以前遇到的一个问题,就是Java除了值传递以外,到底有没有引用传递。网上众说纷纭,我看了具有代表性的10几篇文章,结合书中以及自己的举例,终于得出,Java只有按值传递,没有按引用传递。或者可以说为Java只有副本传递,为什么这么说呢?请看我的论证。


基本数据类型分析

public static void main(String[] args) {
	/** 基本数据类型 **/
    int a = 2;
    p.foo(a);  // 传入基本数据类型,是一个值拷贝
    System.out.println(a);  // 2
}

private void foo(int value) {
    value = 100;
}

Java中所有的基本数据类型结果都一样,在这里用int做一下实验。
可以看出结果并没有改变a的值,因为Java对基本数据类型的传递,首先为拷贝的值开辟一个内存,让value指向拷贝的那个值,所以这是值传递。下面是内存分析图:



包装数据类型分析

/** 包装数据类型 **/
Integer integer = 2;
p.foo(integer); // 包装类作为参数传递时,仍是值传递
System.out.println(integer);    // 2

Integer integer1 = new Integer(2);
p.foo(integer1);    // 包装类作为参数传递时,仍是值传递
System.out.println(integer1);   // 2

private void foo(Integer value) {
        value = 100;
}

Java中所有的包装数据类型和基本数据类型之间都会完成隐式的互相转换,这也是为什么list可以add基本数据类型的原因。
同样这个例子和基本数据类型的结果是一样的,传递的也是值的拷贝。内存分析和上图一样。

String字符串

/** String **/
String b = "mac";
p.fooString(b);   // 没有提供改变自身方法的引用类型
System.out.println(b);  // mac

String b2 = new String("mac");
p.fooString2(b2);
System.out.println(b2); // mac

String b3 = "mac";
b3.replaceAll("m", "M");
System.out.println(b3); // mac

private void fooString(String value) {
   value = "windows";
}

private void fooString2(String value) {
   value.concat(" computer");
}

字符串由于其特殊性(字符串是不可变的),实际上方法是对原来的对象进行了一个拷贝,之后的操作都是在拷贝对象里进行的。所以不影响原来对象的值。
b和b2对象都是在fooString方法里面进行了一次拷贝,然后value的操作不影响b和b2的值。
b3也是传递了一个拷贝值到fooString2这个方法,然后concat是对这个拷贝值进行操作。



系统对象

/** 系统对象 **/
StringBuffer sb = new StringBuffer("iPhone");
p.foo(sb);  // 提供了改变自身方法的引用类型,但是方法内部拿到地址的引用指向了新的地址.原来sb指向的地址并没有改变
System.out.println(sb); // iPhone

StringBuilder sb2 = new StringBuilder("iPhone");
p.foo(sb2); // 提供了引用地址的copy,改变指向地址对象的值.
System.out.println(sb2);    // iPhone5s

private void foo(StringBuffer buffer) {
    buffer = new StringBuffer("iPad");
}

private void foo(StringBuilder builder) {
    builder.append("5s");
}

其实前面的还好去理解传递是值传递,那么对象这块如何去理解呢?
很多人都在这里认为传递是引用传递,我们来进行一下分析。


上图的sb对象指向了对象StringBuffer,它有个值“iPhone”,传入方法后,buffer其实拿到的不是原来sb的引用,只不过是一个副本而已。尽管它也指向iPhone的内存地址。
第一个例子里面,buffer在方法里面重新指向了一个新的StringBuffer对象的内存地址,放着一个值”iPad”,所以原来sb指向的值并没有改变。

第二个例子里面,builder拿到了原来对象的引用拷贝,但是指向的地址不变,所以可以修改内存地址中的对象的属性,并没有新的引用。



所以可以看出来,我们并没有拿到引用去操作内存地址中存在的值,只不过是一个引用的拷贝,如果这个例子说的还不够信服的话,下面的例子应该会让人信服。

自定义对象

/** 自定义对象 **/
Employee e1 = new Employee("tom", 7000);
Employee e2 = new Employee("jerry", 4000);
p.swap1(e1, e2);
System.out.println(e1);	// tom 7000
System.out.println(e2);	// jerry 4000

p.swap2(e1, e2);
System.out.println(e1); // caroline 7000
System.out.println(e2); // benjamin 4000

private void swap1(Employee x, Employee y) {
    Employee temp = x;
    x = y;
    y = temp;
}

private void swap2(Employee x, Employee y) {
    Employee temp = x;
    x = y;
    y = temp;
    x.setName("benjamin");
    y.setName("caroline");
}

Employee实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package JavaCoreTechnology;

/**
 * Created by benjamin on 1/14/16.
 */
public class Employee {
    private String name;
    private int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String toString() {
        return this.name + " " + this.salary;
    }

    public void setName(String name) {
        this.name = name;
    }
}


上图是swap1的交换的实现,这也正说明了为什么输出的值并没有改变。因为原来对象的引用并没有改变,改变的只有这个引用的副本。
这也正说明了为什么swap2是改变的交换的对象的值。

总结:
在Java中并不像c++的那样通过指针操作变量,虽然在Java的最底层仍然使用的是指针,但是Java的开发者已经帮我们封装好了,让我们接触不到而已。基本数据类型的值传递的方式还是和c++的方式一样,是一个值的拷贝。操作的是拷贝的值不会对原来的值造成影响。
而对象的操作则是引用的拷贝,其实也是一个值,是内存地址的值而已。它没有完全把引用传递。所以翻转它的时候不会对原来的对象造成影响,只不过他们指向的都是同一个地址,会改变内存地址中的值罢了。

所以!Java只有按值传递!