Fork me on GitHub

集合中的深拷贝

深拷贝与浅拷贝

对对象的深拷贝和浅拷贝我们并不陌生。

(1)浅拷贝:

在浅拷贝中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅拷贝中,当对象被复制时只复制它本身和其他包含的值类型的成员变量,而引用类型的成员对象并没有复制。在Java中,通过覆盖Object类的clone()方法可以实现浅拷贝。

(2)深拷贝:

在深拷贝中,无论原型对象的成员变量是值对象还是引用类型,都将复制一份给克隆对象,深拷贝将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。在Java中,如果要实现深拷贝,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。(当然如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式实现对象的深克隆。)

这里肯定有些朋友注意到Cloneable接口和Serializable接口,这两个接口其实都是java中的标记型接口,其中都没有任何方法的定义,其实就是告诉jre这些接口的实现类是否有某个功能,是否支持克隆,是否支持序列化。

Object类中的clone()方法

可以看到这个方法在Object类中是protected的。可与看看它的方法的源码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* x.clone() != x
* will be true, and that the expression:
* x.clone().getClass() == x.getClass()
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* x.clone().equals(x)
* will be {@code true}, this is not an absolute requirement.
* <p>
*/

从注释中我们可以看到:

  1. 因为x.clone != x,所以我们可以知道克隆对象将有单独的内存地址分配。
  2. 原始对象和克隆对象有相同的类型,但它不是强制的。
  3. 原始和克隆对象应该是平等的,equal()方法返回true,但它不是强制的。

同时,可以猜想到clone()方法设置为protected的原因:每个类都去继承了Object方法,但是clone提供的是浅拷贝的功能,我们并不知道要拷贝的原始类中成员都是值对象还是引用对象,如果要在一个类B中使用一个非同包类A的clone对象,并且B没有继承A,那么这时是不能调用A.clone(),因为Object的clone方法是protected的,如果要调用则需要A去重写clone方法,并且把clone方法的可见范围变为B类能访问的可见范围。这样A就可以对自身的成员变量是否存在引用对象去判断到底是提供深拷贝还是浅拷贝克隆方法,从而不会把自己的引用成员变量暴露出去被修改。关于这个详细的解释有篇博客:clone方法为什么是protected的?

浅拷贝和深拷贝的实现

浅拷贝

浅拷贝的实现:

  1. 实现Cloneable接口(不实现的话在clone方法会抛出ClassNotSupportedException异常),虽然该接口只是一个标记型接口。
  2. 覆盖clone方法,访问修饰符是public。方法中调用supers.clone()方法得到需要的复制对象。(native本地方法,效率还是很高的)。

demo

来一个无论学习什么知识都会有的User类,让其实现Cloneable接口且覆盖父类的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
public class User implements Cloneable{

// Cloneable是一个标记型接口,不实现在进行拷贝的时候会报错

private int number;

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

@Override
public String toString() {
return "User{" +
"number=" + number +
'}';
}

/**
* 重写Object的clone方法
*
* 改修饰权限符为public
*
* 直接调用super.clone() 这个native方法进行拷贝对象
*/

@Override
public User clone() throws CloneNotSupportedException {
return (User) super.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
/**
* test方法
*/
public static void main(String[] args) {

User user1 = new User();

user1.setNumber(1111);

User user2 = null;
// 实现浅拷贝
try {
user2 = user1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

if (null != user2) {
// 变更浅拷贝出的对象中的成员变量
user2.setNumber(2222);-
}

System.out.println(user1);// 输出1111,浅拷贝
System.out.println(user2);
}

深拷贝

在User类中加入一个引用对象Address类

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
50
51
52
53
54
55
56
57
58
59
60
61
public class User implements Cloneable{

// Cloneable是一个标记型接口,不实现在进行拷贝的时候会报错

private int number;

/**
* 引用对象
*/
private Address address;

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}

public User(int number, Address address) {
this.number = number;
this.address = address;
}

@Override
public String toString() {
return "User{" +
"number=" + number +
", address=" + address +
'}';
}

/**
* 覆盖clone方法
* 改为public
*
* 返回值为user
*
* @return
*/
@Override
public User clone() {
User user = null;

try {
user = (User) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

return user;
}
}

Address类中并没有实现Cloneable接口和覆盖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
public class Address {

private String add;

public String getAdd() {
return add;
}

public void setAdd(String add) {
this.add = add;
}


public Address(String add) {
this.add = add;
}

@Override
public String toString() {
return "Address{" +
"add='" + add + '\'' +
'}';
}
}

这时候测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {

Address address = new Address("1111");
User user1 = new User(1111, address);

User user2 = user1.clone();

System.out.println(user1); // 都输出1111
System.out.println(user2);

// 修改user2的成员变量
address.setAdd("2222");

// 因为现在还是浅拷贝 user1 和 user2中的值还指向堆内存中同一个address对象
System.out.println(user1);
System.out.println(user2);

}

// 返回结果
User{number=1111, address=Address{add='2222'}}
User{number=1111, address=Address{add='2222'}}

可以看到当改变add的值的时候,user1和user2中的address对象都发生了改变,这里是因为user2只是user1的浅拷贝生成的对象,user2对象中的address成员变量只是复制了user1中的address变量的引用, 并没有将user2中的address对象指向另一块Address对象。

这里可以去改造下,Address对象要实现Cloneable接口并且覆盖clone方法,并且User对象中的clone方法也要定义user对象中的address成员变量的拷贝

Address中的clone

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Address clone() {
// 实现clone方法
Address address = null;
try {
address = (Address) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

return address;
}

User中的clone:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 */
@Override
public User clone() {
User user = null;

try {
user = (User) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

// 对引用类型的成员变量进行clone
user.address = address.clone();
return user;
}

这样发现运行的结果:

1
2
3
4
5
User{number=1111, address=Address{add='1111'}}
User{number=1111, address=Address{add='1111'}}
====================变更了address====================
User{number=1111, address=Address{add='2222'}}
User{number=1111, address=Address{add='1111'}}

实现深拷贝的方法

重写clone方法

在上面的测试中,我们可以知道重写clone可以实现深拷贝,但是当我们遇到成员变量是一个嵌套对象的时候也要求内层嵌套对象也要重写clone方法,这样比较麻烦,我们可以采用序列化的方式去做深拷贝

序列化方式

序列化方式下,我们需要让需要拷贝的对象类实现序列化接口,然后通过流或者Json等序列化工具去深拷贝一个对象。

一个基于流的深拷贝工具类

这里提供一个基于流的深拷贝工具类,这里场景是根据

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
@SuppressWarnings("unchecked")
@Slf4j
public class DeepCopyUtil {

/**
* 对集合进行深拷贝
*
* @param src
* @param <T>
* @return
*/
public @Nullable static <T> Collection<T> deepCopy(Collection<T> src) {

if (CollectionUtils.isEmpty(src)) {
return null;
}

try {

ByteArrayOutputStream byteOut = new ByteArrayOutputStream();

ObjectOutputStream out = new ObjectOutputStream(byteOut);

out.writeObject(src);

ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());

ObjectInputStream in = new ObjectInputStream(byteIn);

return (Collection<T>) in.readObject();

} catch (ClassNotFoundException | IOException e) {
log.warn("deep copy fail, src={} ", JSON.toJSONString(src), e);
return null;
}
}

}

其实还是比较建议直接用FastJson或者其他序列化工具去序列化来实现深拷贝。

github

该部分代码:https://github.com/zhanglijun1217/java8/tree/master/src/deep_and_shallow_copy

-------------本文结束感谢您的阅读-------------

本文标题:集合中的深拷贝

文章作者:夸克

发布时间:2018年12月19日 - 22:12

最后更新:2022年07月01日 - 05:07

原始链接:https://zhanglijun1217.github.io/2018/12/19/集合中的深拷贝/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。