Pass by value or Pass by reference?

拷贝-引用拷贝

1.这里定义一个User类,给定nameage属性,测试打印一些信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class User implements Cloneable {
private int age;

private String name;

public User(int age, String name) {
this.age = age;
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

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

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

public static void main(String[] args) {
User user = new User(18, "Sai");
User otherUser = user;
System.out.println("user = " + user + "\n" + "user name = " + user.getName());
System.out.println("otherUser = " + otherUser + "\n" + "otherUser name " + otherUser.getName());
}

2.打印信息发现他们指向了同一个内存地址(指向同一个对象):

user = User@7852e922
user name = Sai
otherUser = User@7852e922
otherUser name Sai

3.需要明确的是User user这里的user存在是在栈区内,而对象在被真正创建的时候new User()此时开辟内存是在堆内存当中。Java的堆内存是共享的,而栈不是共享的。

拷贝-对象拷贝

1.还是根据上述的例子修改main中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
User user = new User(18, "Sai");
User otherUser = null;
try {
otherUser = (User) user.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println("user = " + user + "\n" + "user name = " + user.getName());
assert otherUser != null;
System.out.println("otherUser = " + otherUser + "\n" + "otherUser name " + otherUser.getName());
}

2.查看打印信息,虽然姓名年龄的值都是一样的,内存地址是不同的,也即两个引用指向了堆中两个不同的对象,但是对象的属性是相同的:

1
2
3
4
5
//打印的信息
user = User@7852e922
user name = Sai
otherUser = User@4e25154f
otherUser name Sai

浅拷贝

1.浅拷贝:拷贝后的对象的变量或者属性值仍然跟原对象相同,但是被拷贝的对象的中引用还是指向原来的地址。举个栗子,User类中还有持有了另外一个对象Boll,这个对象的地址假设为0x888272,现在浅拷贝User对象。otherUser,按照定义此时userotherUser是两个不同的对象(这么说不严谨,准确的说是指向了不同内存地址),但是他们的持有的Boll对象的地址都为0x888272

深拷贝

2.深拷贝:按照之前的浅拷贝定义,深拷贝将对象中引用的对象也拷贝了一份,可见,深拷贝的性能开销要比浅拷贝要大。速度要更慢,那么如果执行深拷贝,上述持有Boll对象的地址会发生改变,完全复制了一份。实际开发过程中,序列化就是一个深拷贝的过程。

正题

1.经常会听到有人对Java到底是Pass by value还是Pass by reference疑惑不已。这个确实很令人困惑,但是需要明确的是Java是值传递(Pass by value),这个是毋庸置疑的,只是平时一些例子看上去好像是引用传递Java中数据类型概括来说无非就是基本数据类型(八种基本数据类型),引用类型。

2.基本数据类型的传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
int num = 20;
//此时num为实参
afterChange(num);
System.out.println("afterChange = " + num);
}

//这里的m为形参
private static void afterChange(int m) {
m = m + 10;
System.out.println("m = " + m);
}

//打印信息
> Task :Sai.main()
m = 30
afterChange = 20

3.通过打印信息,可以发现,方法afterChange虽然对传入的实参做了增加操作,但是却没有改变原来的值num。在传递的过程只不过是将num的值拷贝了一份,而对这个拷贝的值操作自然是无法改变原值num的,这么说来Java好像是值传递的(Pass by value)。但是继续看另外一个例子。引用类型的传递。

4.定义一个Dog类做一些测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Dog {

private String name;

private String color;

public Dog(String name, String color) {
this.name = name;
this.color = color;
}

public String getName() {
return name;
}

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

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}
}

public static void main(String[] args) {
Dog dog = new Dog("Python", "Black");
dogTest(dog);
System.out.println("After dogTest the dog name is " + dog.getName());
}

private static void dogTest(Dog dog) {
if (null != dog) {
dog.setName("Jia");
System.out.println("the dog name is " + dog.getName());
}
}

> Task :Sai.main()
the dog name is Jia
After dogTest the dog name is Jia

打印的信息居然是一样的也就是name这个属性的值被改了,看到这个是不是又怀疑人生了,也许反应是这样:
tiDLpq.png
,难道当传递的是引用类型的时候就变成引用传递了么?相信很多人刚开始接触Java的时候也许会有这样的疑惑。但是需要指出的是,这里还是值的传递。一步步解释。

当我们定义Dog dog;是,需要明白的是此时不是实例仅仅只是存在与栈中的变量,指向堆中一个真实对象Dog但是具体指向哪个还不确定,因为真实的Dog还未被创建。
Dog dog = new Dog();此时对象在堆中被创建,而dog是对象的引用,假设地址为0x123。改一下测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
Dog dog = new Dog("Python", "Black");
System.out.println("address = " + dog);
dogTest(dog);
System.out.println("After dogTest the dog name is " + dog.getName());
System.out.println("address = " + dog);
}

private static void dogTest(Dog dog) {
if (null != dog) {
dog = new Dog("ObjectC", "Red");
System.out.println("the dog name is " + dog.getName());
System.out.println("address = " + dog);
}
}

> Task :Sai.main()
//执行dogTest方法之前dog对应的堆中地址
address = Dog@7852e922
the dog name is ObjectC
address = Dog@4e25154f
After dogTest the dog name is Python
//执行dogTest方法之前dog对应的堆中地址,可以发现是相同的地址,dog指向的地址未发生改变。
address = Dog@7852e922

这看上去又像是值传递了,但是疑问又来了,我这里重新创建了一个Dog实例,地址当然跟原始的不一样,是不是这个实例化操作用在这里证明不够恰当?但是我们假设,是引用传递,我们在dog = new Dog("ObjectC", "Red");是否可以这样理解,这里改变了原dog的指向地址,可是事实main中的dog指向的地址是没有改变的。显然不是引用传递。传递的只是对引用地址的一份拷贝,既然是拷贝,那么方法体中对这个拷贝引用的修改会反应到堆中的对象上,另外方法体外部同样指向这个对象。既然被改变了,也是可感知的。

举个栗子

1.你和女朋友租的一套房子,但是只有你一把钥匙,领导安排你出差一天。于是决定复制一把钥匙给女朋友(传递拷贝),女朋友比较勤快将房子打扫一番(对对象属性修改)等你回家。这是你回家之后感觉到的。可是不小心的是,女朋友将钥匙丢了,只能在家呆着出不了门。但是这并不影响,你手中的钥匙(原引用)还能开门的事实,你回来开门进去就好了。有一个bug(程序员没有女朋友)。
tiquy8.md.png

StackOverflow

1.StackOverflow上关于这个问题的讨论有很多不错的描述:

Think about it this way. Someone has the address of Ann Arbor, MI (my hometown, GO BLUE!) on a slip of paper called “annArborLocation”. You copy it down on a piece of paper called “myDestination”. You can drive to “myDestination” and plant a tree. You may have changed something about the city at that location, but it doesn’t change the LAT/LON that was written on either paper. You can change the LAT/LON on “myDestination” but it doesn’t change “annArborLocation”.

StackOverflow

这个功能是摆设,看看就好~~~