Java深拷贝和浅拷贝

对象拷贝有哪些

对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部数据。

Java中有三种类型的对象拷贝:

  • 浅拷贝(Shallow Copy)
  • 深拷贝(Deep Copy)
  • 延迟拷贝(Lazy Copy)

理解浅拷贝

什么是浅拷贝?
  • 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

    • 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

    • http://static.cyblogs.com/QQ20200226-210909@2x.jpg

    • 在上图中,SourceObject有一个int类型的属性 “field1”和一个引用类型属性”refObj”(引用ContainedObject类型的对象)。当对SourceObject做浅拷贝时,创建了CopiedObject,它有一个包含”field1”拷贝值的属性”field2”以及仍指向refObj本身的引用。由于”field1”是基本类型,所以只是将它的值拷贝给”field2”,但是由于”refObj”是一个引用类型, 所以CopiedObject指向”refObj”相同的地址。因此对SourceObject中的”refObj”所做的任何改变都会影响到CopiedObject。

如何实现浅拷贝

下面来看一看实现浅拷贝的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Subject {

private String name;
public Subject(String s) {
name = s;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}
}
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
public class Student implements Cloneable { 

// 对象引用
private Subject subj;
private String name;

public Student(String s, String sub) {
name = s;
subj = new Subject(sub);
}

public Subject getSubj() {
return subj;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}

/**
* 重写clone()方法
* @return
*/
public Object clone() {
//浅拷贝
try {
// 直接调用父类的clone()方法
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CopyDemoMain {
public static void main(String[] args) {
// 原始对象
Student stud = new Student("杨充", "潇湘剑雨");
System.out.println("原始对象: " + stud.getName() + " - " + stud.getSubj().getName());

// 拷贝对象
Student clonedStud = (Student) stud.clone();
System.out.println("拷贝对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());

// 原始对象和拷贝对象是否一样:
System.out.println("原始对象和拷贝对象是否一样: " + (stud == clonedStud));
// 原始对象和拷贝对象的name属性是否一样
System.out.println("原始对象和拷贝对象的name属性是否一样: " + (stud.getName() == clonedStud.getName()));
// 原始对象和拷贝对象的subj属性是否一样
System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (stud.getSubj() == clonedStud.getSubj()));

stud.setName("小杨逗比");
stud.getSubj().setName("潇湘剑雨大侠");
System.out.println("更新后的原始对象: " + stud.getName() + " - " + stud.getSubj().getName());
System.out.println("更新原始对象后的克隆对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName());
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
Connected to the target VM, address: '127.0.0.1:57836', transport: 'socket'
原始对象: 杨充 - 潇湘剑雨
拷贝对象: 杨充 - 潇湘剑雨
原始对象和拷贝对象是否一样: false
原始对象和拷贝对象的name属性是否一样: true
原始对象和拷贝对象的subj属性是否一样: true
更新后的原始对象: 小杨逗比 - 潇湘剑雨大侠
更新原始对象后的克隆对象: 杨充 - 潇湘剑雨大侠
Disconnected from the target VM, address: '127.0.0.1:57836', transport: 'socket'

可以得出的结论

  • 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。从输出结果中我们可以看到,对原始对象stud的”name”属性所做的改变并没有影响到拷贝对象clonedStud,但是对引用对象subj的”name”属性所做的改变影响到了拷贝对象clonedStud。

理解深拷贝

什么是深拷贝?
  • 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
    • http://static.cyblogs.com/QQ20200226-212333@2x.jpg
    • 在上图中,SourceObject有一个int类型的属性 “field1”和一个引用类型属性”refObj1”(引用ContainedObject类型的对象)。当对SourceObject做深拷贝时,创建了CopiedObject,它有一个包含”field1”拷贝值的属性”field2”以及包含”refObj1”拷贝值的引用类型属性”refObj2” 。因此对SourceObject中的”refObj”所做的任何改变都不会影响到CopiedObject
实现深拷贝案例

下面是实现深拷贝的一个例子。只是在浅拷贝的例子上做了一点小改动,Subject 和CopyTest 类都没有变化。

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
public class DeepStudent implements Cloneable {
// 对象引用
private Subject subj;
private String name;

public DeepStudent(String s, String sub) {
name = s;
subj = new Subject(sub);
}

public Subject getSubj() {
return subj;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}

/**
* 重写clone()方法
*
* @return
*/
public Object clone() {
// 深拷贝,创建拷贝类的一个新对象,这样就和原始对象相互独立
DeepStudent s = new DeepStudent(name, subj.getName());
return s;
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
Connected to the target VM, address: '127.0.0.1:60802', transport: 'socket'
原始对象: 杨充 - 潇湘剑雨
拷贝对象: 杨充 - 潇湘剑雨
原始对象和拷贝对象是否一样: false
原始对象和拷贝对象的name属性是否一样: true
原始对象和拷贝对象的subj属性是否一样: true
更新后的原始对象: 小杨逗比 - 潇湘剑雨大侠
更新原始对象后的克隆对象: 杨充 - 潇湘剑雨大侠
Disconnected from the target VM, address: '127.0.0.1:60802', transport: 'socket'

得出的结论

  • 很容易发现clone()方法中的一点变化。因为它是深拷贝,所以你需要创建拷贝类的一个对象。因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。

序列化进行拷贝

序列化属于深拷贝

可能你会问,序列化是属于那种类型拷贝?答案是:通过序列化来实现深拷贝。可以思考一下,为何序列化对象要用深拷贝而不是用浅拷贝呢?

注意要点

可以序列化是干什么的?它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。

序列化案例

看一下下面案例,很简单,只需要实现Serializable这个接口。Android中还可以实现Parcelable接口。

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
public class ColoredCircle implements Serializable { 

private int x;
private int y;

public ColoredCircle(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

@Override
public String toString() {
return "x=" + x + ", y=" + y;
}
}
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
public class DouBi implements Serializable {

private static final long serialVersionUID = -8752043194511106066L;

private int x;
private int y;

public DouBi(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}
}
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
45
46
47
48
49
public class CopyDemoMain3 {
public static void main(String[] args) {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// 创建原始的可序列化对象
DouBi c1 = new DouBi(100, 100);
System.out.println("原始的对象 = " + c1);
DouBi c2 = null;
// 通过序列化实现深拷贝
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// 序列化以及传递这个对象
oos.writeObject(c1);
oos.flush();
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bin);
// 返回新的对象
c2 = (DouBi) ois.readObject();
// 校验内容是否相同
System.out.println("复制后的对象 = " + c2);
// 改变原始对象的内容
c1.setX(200);
c1.setY(200);
// 查看每一个现在的内容
System.out.println("查看原始的对象 = " + c1);
System.out.println("查看复制的对象 = " + c2);
} catch (IOException e) {
System.out.println("Exception in main = " + e);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

输出结果如下:

1
2
3
4
5
6
Connected to the target VM, address: '127.0.0.1:62095', transport: 'socket'
原始的对象 = com.vernon.test.copy.DouBi@1134affc
复制后的对象 = com.vernon.test.copy.DouBi@1b604f19
查看原始的对象 = com.vernon.test.copy.DouBi@1134affc
查看复制的对象 = com.vernon.test.copy.DouBi@1b604f19
Disconnected from the target VM, address: '127.0.0.1:62095', transport: 'socket'

注意:需要做以下几件事儿:

  • 确保对象图中的所有类都是可序列化的

  • 创建输入输出流

  • 使用这个输入输出流来创建对象输入和对象输出流

  • 将你想要拷贝的对象传递给对象输出流

  • 从对象输入流中读取新的对象并且转换回你所发送的对象的类

  • 得出的结论

    • 在这个例子中,创建了一个DouBi对象c1然后将它序列化 (将它写到ByteArrayOutputStream中). 然后我反序列化这个序列化后的对象并将它保存到c2中。随后我修改了原始对象c1。然后结果如你所见,c1不同于c2,对c1所做的任何修改都不会影响c2。
    • 注意,序列化这种方式有其自身的限制和问题:因为无法序列化transient变量, 使用这种方法将无法拷贝transient变量。再就是性能问题。创建一个socket, 序列化一个对象, 通过socket传输它, 然后反序列化它,这个过程与调用已有对象的方法相比是很慢的。所以在性能上会有天壤之别。如果性能对你的代码来说是至关重要的,建议不要使用这种方式。它比通过实现Clonable接口这种方式来进行深拷贝几乎多花100倍的时间。

延迟拷贝

  • 延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。这个以前几乎都没听说过,后来看书才知道有这么一种拷贝!
  • 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
  • 延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且, 在某些情况下, 循环引用会导致一些问题。

如何选择拷贝方式

  • 如果对象的属性全是基本类型的,那么可以使用浅拷贝。
  • 如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。
  • 意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。

数组的拷贝

  • 数组除了默认实现了clone()方法之外,还提供了Arrays.copyOf方法用于拷贝,这两者都是浅拷贝。
基本数据类型数组

如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
public void test4() {
int[] lNumbers1 = new int[5];
int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length);
rNumbers1[0] = 1;
boolean first = lNumbers1[0] == rNumbers1[0];
Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers1[0] + ",rNumbers2[0]=" + rNumbers1[0]+"---"+first);

int[] lNumbers3 = new int[5];
int[] rNumbers3 = lNumbers3.clone();
rNumbers3[0] = 1;
boolean second = lNumbers3[0] == rNumbers3[0];
Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"---"+second);
}

打印结果如下所示

1
2
2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小杨逗比: lNumbers2[0]=0,rNumbers2[0]=1---false
2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小杨逗比: lNumbers3[0]=0,rNumbers3[0]=1---false

引用数据类型数组

如下所示

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
public static void test5() {
People[] lNumbers1 = new People[5];
lNumbers1[0] = new People();
People[] rNumbers1 = lNumbers1;
boolean first = lNumbers1[0].equals(rNumbers1[0]);
Log.d("小杨逗比", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]+"--"+first);

People[] lNumbers2 = new People[5];
lNumbers2[0] = new People();
People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
boolean second = lNumbers2[0].equals(rNumbers2[0]);
Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]+"--"+second);

People[] lNumbers3 = new People[5];
lNumbers3[0] = new People();
People[] rNumbers3 = lNumbers3.clone();
boolean third = lNumbers3[0].equals(rNumbers3[0]);
Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"--"+third);
}

public static class People implements Cloneable {

int age;
Holder holder;

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

public static class Holder {
int holderValue;
}
}

打印日志如下

1
2
3
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18,rNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18--true
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671,rNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671--true
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56,rNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56--true

集合的拷贝

  • 集合的拷贝也是我们平时经常会遇到的,一般情况下,我们都是用浅拷贝来实现,即通过构造函数或者clone方法。
集合浅拷贝

构造函数和 clone() 默认都是浅拷贝

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
public static void test6() {
ArrayList<People> lPeoples = new ArrayList<>();
People people1 = new People();
lPeoples.add(people1);
Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0));
ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone();
Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0));
boolean b = lPeoples.get(0).equals(rPeoples.get(0));
Log.d("小杨逗比", "比较两个对象" + b);
}

public static class People implements Cloneable {

int age;
Holder holder;

@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

public static class Holder implements Cloneable {

int holderValue;

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}

打印日志

1
2
3
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: 比较两个对象true
集合深拷贝

在某些特殊情况下,如果需要实现集合的深拷贝,那就要创建一个新的集合,然后通过深拷贝原先集合中的每个元素,将这些元素加入到新的集合当中。

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
45
46
47
public static void test7() {
ArrayList<People> lPeoples = new ArrayList<>();
People people1 = new People();
people1.holder = new People.Holder();
lPeoples.add(people1);
Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0));
ArrayList<People> rPeoples = new ArrayList<>();
for (People people : lPeoples) {
rPeoples.add((People) people.clone());
}
Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0));
boolean b = lPeoples.get(0).equals(rPeoples.get(0));
Log.d("小杨逗比", "比较两个对象" + b);
}

public static class People implements Cloneable {

int age;
Holder holder;

@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}

public static class Holder implements Cloneable {

int holderValue;

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}

打印日志

1
2
3
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: 比较两个对象false

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号