拷贝-引用拷贝
1.这里定义一个User
类,给定name
和age
属性,测试打印一些信息: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
38public 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;
}
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
12public 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
,按照定义此时user
与otherUser
是两个不同的对象(这么说不严谨,准确的说是指向了不同内存地址),但是他们的持有的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
17public 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
44public 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
这个属性的值被改了,看到这个是不是又怀疑人生了,也许反应是这样:
,难道当传递的是引用类型的时候就变成引用传递
了么?相信很多人刚开始接触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
24public 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(程序员没有女朋友)。
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”.