赋值、浅拷贝与深拷贝

引言

前几天写Java时遇到一个问题,在我的代码中有这样一段:

1
2
3
4
5
6
7
8
9
10
11
Set<Person> Temp = new HashSet<Person>();
Set<Person> RemainedPerson = new HashSet<Person>();
RemainedPerson.add(vertexA); // vertexA is an instance of Person class.

while (!RemainedPerson.isEmpty()) {
for (Person person: RemainedPerson) {
// Do something that will change Temp.
}
RemainedPerson = Temp;
Temp.clear();
}

我本来是想用Temp临时保存一些Person对象,当for循环结束后再将Temp中保存的对象拷贝到RemainedPerson中,然后将Temp清空进行下一次while循环。但是实际上明明Temp里被加入了对象却依然会提前退出循环。我意识到可能是Temp.clear()将Temp清空的同时也将RemainedPerson清空了。一试果然如此!看来这里赋值后Temp和RemainedPerson应该是同一个对象,对一个进行操作另一个也会随之改变。

Java中的赋值、浅拷贝与深拷贝

  • 要搞清上面说的问题为何会发生,首先要了解Java中的数据类型有哪些

Java中的数据类型分为基本数据类型和引用数据类型两大类,基本数据类型只有

1
boolean, byte, char, short, int, long, float, double

这8种,除此之外的所有类型都属于引用类型。可以这么理解,基本数据类型就跟C语言中的数据类型一样,在定义时就会被分配空间,每一个变量都拥有自己独立的内存空间(1)。而引用数据类型就类似于C语言中的指针,定义时只分配指针的空间,而不会分配指针指向的内存空间,只有当使用 new 实例化对象时才会分配空间。

不过这里有一个要注意的问题是对于String类型,虽然它不属于基本数据类型,但是当采用如下的声明方式时:

1
String str = "abc";

应该把它当作基本数据类型来处理。

  • 接下来还需要知道Java中如何将一个变量的值赋给另一个变量

Java中要将一个变量的值赋给另一个变量有两种方法,一种是用”=”号直接赋值,另一种是调用对象的clone方法(前提是对象实现了Cloneable接口)。对于基本数据类型只能使用”=”号进行赋值;而对于是实现了Cloneable接口的引用数据类型除了可以使用”=”号赋值外,还可以使用clone方法将一个对象的值拷贝给另一个同类型的对象,只不过这两种方法的操作结果是有区别的。

赋值

对于基本数据类型,由于变量拥有自己独立的内存空间,所以赋值时是进行值拷贝,故如果把变量A的值赋给变量B,然后改变变量A的值并不会对B造成影响,这与我们的预期是相符的。

但是对于引用数据类型却并非如此。当使用”=”号将一个引用类型变量的值赋给另一个同类型的引用类型变量时,传递的只是对对象的引用。也就是说对于以下代码:

1
2
3
ReferenceType A, B;
B = new ReferenceType();
A = B;

得到的A和B其实是对同一个对象的引用,或者可以理解为A和B其实是同一个指针,指向内存中的同一块内存。

但是这里有一个问题,就是如果声明A时就直接调用类的构造方法将其实例化了呢?这样A就有了自己的内存空间了,会不会在赋值时就进行值拷贝而不是引用拷贝呢?事实证明无论A有没有实例化,使用”=”号进行赋值时都是进行引用拷贝而非值拷贝。

所以,总结来说就是,使用”=”赋值时,如果变量是基本数据类型的,那就进行值拷贝,每一个变量都有自己独立的内存空间,互不干扰;而对于引用数据类型,则进行引用拷贝,”=”两边的两个变量将指向同一个对象,对一个进行操作就会影响到另一个。

浅拷贝与深拷贝

浅拷贝和深拷贝都只针对于引用数据类型,基本数据类型不存在深浅拷贝的问题。

clone方法简介

clone方法是Object类中的一个基本方法,由于Object是所有类的根类,所以任何一个Java对象都是有clone方法的。但是clone方法在Object中是声明为protected的,所以默认情况下我们不能调用clone方法来克隆对象。当需要使用clone方法时,需要实现Cloneable接口、重写(Override)clone方法并将它的修饰符改为public。这个时候就有两种选择了:一是直接调用父类的clone方法;二是自己重写clone方法。

浅拷贝

如果直接调用父类的clone方法,那么此时的拷贝就是浅拷贝(Shadow Copy)。

在浅拷贝中,被拷贝对象的在内存中数据会被原封不动的复制一份到目标对象的内存空间中。注意这个原封不动!何谓原封不动?就是说原来什么样现在还是什么样,不发生任何变化。乍一看上去这很合理,拷贝嘛,本来就该原封不动。但是这里有一个问题,就是Java中引用数据类型的变量保存的是对对象的引用,而非对象的实体!

所以如果一个对象A的某个属性是另一个对象B,那么在A的内存空间中保存的并不是对象B,而是对对象B的引用。当我们调用A的clone方法将A的值拷贝给同类型对象C时,对象A的数据会原封不动的被复制一份给C,其中就包括对对象B的引用。所以拷贝之后C的内存空间中保存的也将是对B的引用。那么如果我们此时修改对象B,A和C将一起受到影响。

举个例子,我们假设有一个类Person:

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
public class Person implements Cloneable {
private int Age;
private String Name;

public Person(int Age, String Name) {
this.Age = Age;
this.Name = Name;
}

public int getAge() {
return this.Age;
}

public String getName() {
return this.Name;
}

public String toString() {
return this.getName();
}

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

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

有另一个类Team:

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
import java.util.Set;
import java.util.HashSet;

public class Team implements Cloneable {
private String Name;
private Person Leader;

public Team(String Name, Person Leader) {
this.Name = Name;
this.Leader = Leader;
}

public String getName() {
return this.Name;
}

public Person getLeader() {
return this.Leader;
}

public String toString() {
return this.Leader.getName() + this.getName();
}

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

当我们执行如下代码时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) {
String Name = "Team";
Person Leader = new Person(30, "Leader");
Team MyTeam = new Team(Name, Leader);
Team AnotherTeam;

try{
AnotherTeam = (Team)MyTeam.clone();
System.out.println(MyTeam);
System.out.println(AnotherTeam);

Leader.setName("NewName");
System.out.println(MyTeam);
System.out.println(AnotherTeam);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}

输出会是:

1
2
3
4
LeaderTeam
LeaderTeam
NewNameTeam
NewNameTeam

可以看到,当改变Leader的名字时,两个Team对象都改变了。这说明两个Team对象中的Leader是对同一个对象的引用。

深拷贝

相对浅拷贝来说,深拷贝在拷贝对象时会把对象中引用的其他对象一同拷贝一份。还是用上面的例子,如果Team对象的clone方法是深拷贝的话,那么拷贝后的AnotherTeam中的Leader属性就指向了一个完全全新的Person对象,而不是与MyTeam共享的同一个Person对象,修改程序中定义的Leader变量不会对AnotherTeam造成任何影响。也就是说上述程序的输出结果会变成:

1
2
3
4
LeaderTeam
LeaderTeam
NewNameTeam
LeaderTeam

但是,之前也说了,如果直接调用父类的clone方法,那么此时的拷贝就是浅拷贝(Shadow Copy)。换言之,如果想要进行深拷贝,需要自己实现clone方法。例如,将上面例子中的Team类改成如下这样:

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
import java.util.Set;
import java.util.HashSet;

public class Team implements Cloneable {
private String Name;
private Person Leader;

public Team(String Name, Person Leader) {
this.Name = Name;
this.Leader = Leader;
}

public String getName() {
return this.Name;
}

public Person getLeader() {
return this.Leader;
}

public String toString() {
return this.Leader.getName() + this.getName();
}

@Override
public Object clone() throws CloneNotSupportedException {
String NewName = this.Name;
Person NewLeader = (Person)this.Leader.clone();
return new Team(NewName, NewLeader);
}
}

那么再次执行Main中的代码时,输出结果将变成:

1
2
3
4
LeaderTeam
LeaderTeam
NewNameTeam
LeaderTeam

备注

注一

实际上并不一定是这样,这里还涉及到Java的内存管理机制,比较复杂,在本文中没有区分的必要,姑且认为每一个基本类型变量都拥有独立的内存空间。