由一个bug谈谈深浅克隆

由一个bug谈谈深浅克隆

viEcho Lv5

本篇我们来谈谈深浅克隆!

开始之前

最近在改一个bug,构建审批附件数据,由于查询的数据由近3万的量,我们用的是Oracle数据库,数据库查询mybatis做了限制,一次查询最多查1000条,那就需要分批次的去查询数据库,如果是串行的去查,接口很容易就超时了;所以这里用了线程池,然而诡异的是并发去查的时候偶发性的报错,查几千条没有报错,然而数据量一上来就报java.util.ConcurrentModificationException;刚开始以为是线程不安全引起的,将线程操作的集合换成了线程安全的集合后,情况并没有好转,直到我们看到了一个拷贝的代码;
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// --多线程及for循环代码省略
Query query2 = new Query();
CommonCopier.copy(query1,query2);
query2.setRowStart(i*1000+1);
query2.setRowEnd((i+1)*1000);
//-- 使用query2查询数据代码省略

// --CommonCopier 中copy方法
private static CoucurrentHashMap BEAN_COPIERS = new Con


public static void copy(Object srcObj,Object targetObj){
String key = srcObj.getClass().getName()+targetObj.getClass().getName();
BeanCopier copier = null;

if(BEAN_COPIER.contains(key)){
copier = BEAN_COPIER.get(key);
}else {
copier = BeanCopier.create(srcObj.getClass(),targetObj.getClass(),false);
}
copier.copy(srcObj,targetObj,null);
}

这样看,这个拷贝方法也没毛病啊!实体之间拷贝,String Integer等常见数据类型的属性拷贝是没有问题,问题就出在拷贝的实体中有引用数据类型,那么对于引用的数据类型这里其实是浅拷贝的,那么在并发的情况下,对于引用的数据类型,线程1设置值去查的时候,线程二可能去修改了,这就导致了sql动态设置值时出现java.util.ConcurrentModificationException异常!
那么什么是深拷贝什么是浅拷贝呢?接下来,我们一起来看下

概念

  • 浅克隆:把原型对象中的成员变量的值类型的属性都复制一份给克隆对象,并且把成员变量中为引用类型的的引用地址也复制一份给克隆对象;
  • 深克隆:把原型对象中的成员变量的值类型和引用类型都复制一份给克隆对象;

我们知道,克隆一个对象我们只需要对应的实体实现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
35
36
37
38
@Data
@Accessors(chain = true)
public class User implements Cloneable {
private Integer id;

private String name;

private List<String> strList;


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

public class TestClone {

public static void main(String[] args) throws CloneNotSupportedException {
List<String> list1 = new ArrayList<String>();
list1.add("富强");

List<String> list2 = new ArrayList<String>();
list2.add("民主");

User user = new User();
user.setId(1).setName("Echo").setStrList(list1);

User cloneUser = (User)user.clone();
user.setName("imEcho");
cloneUser.getStrList().addAll(list2);

System.out.println("原型对象 user"+user);
//原型对象 userUser(id=1, name=imEcho, strList=[富强, 民主])
System.out.println("拷贝对象 cloneUser" +cloneUser);
//拷贝对象 cloneUserUser(id=1, name=Echo, strList=[富强, 民主])
}
}

根据运行结果,我们知道我们将对象拷贝后,改变了原型对象中值对象name属性的值,接下来又改变了克隆对象中的引用对象strList的值,发现克隆对象中值对象并没有随着原型对象中的值改变而改变,而引用对象却是同步作了更改;那么我们知道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
/**
* 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:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* ...
*/
protected native Object clone() throws CloneNotSupportedException;

从源码可以看到,clone方法是一个受保护的本地方法,我们知道本地方法其实就是直接操作内存,底层是调用C的本地方法,所以操作起来性能很高;

由方法上的注释我们可以解读到:

    1. x.clone() != x 返回为true 因为对于所有对象来说,克隆对象和原型对象实际上都是两个对象,它们不相等
    1. x.clone().getClass() == x.getClass() 按照惯例,拷贝的对象类型应该等于原型对象的类型
    1. x.clone().equals(x) 返回true ,因为拷贝的对象使用equals 比较时它们的值都是相等的

      

深克隆的常见方法

  • 所有的对象的引用类型都实现Cloneable接口,重写clone方法;
  • 通过构造方法实现深拷贝
  • 使用jdk自带的字节流实现深拷贝
  • 使用apache Common lang 包中的SerializationUtils.clone()方式实现深拷贝
  • 使用JSON 工具类实现深拷贝,比如Gson,FastJson等

代码如下:

引用类型实现Cloneable 接口

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
@Data
@Accessors(chain = true)
public class User1 implements Cloneable {
private Integer id;

private String name;

private Student student;

@Override
protected User1 clone() throws CloneNotSupportedException {
User1 user1 = (User1) super.clone();
user1.setStudent(this.student.clone());
return user1;
}
}
@Data
@Accessors(chain = true)
public class Student implements Cloneable{
private String name;

private Integer age;

@Override
protected Student clone() throws CloneNotSupportedException {
return (Student) 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
26
27
28
29
30
31
32
33
@Data
@Accessors(chain = true)
public class User2 {

private Integer id;

private String name;

private Student student;

User2(Integer id,String name,Student student){
this.id =id;
this.name = name;
this.student = student;
}

public static void main(String[] args) {
User2 user2 = new User2();
new User2(user2.getId(),user2.getName(),new Student(user2.getStudent().getName(),user2.getStudent().getAge()));
}
}
@Data
@Accessors(chain = true)
public class Student{
private String name;

private Integer age;

Student(String name,Integer age){
this.name = name;
this.age = 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
// 此方法需要克隆对象实现序列化
public static <T extends Serializable> T clone(T obj) throws IOException {
T cloneObj = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(obj);

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ois = new ObjectInputStream(bais);

cloneObj = (T)ois.readObject();
}catch(Exception e){
e.printStackTrace();
}finally{
if(null!=oos){
oos.close();
}
if(null!=ois){
ois.close();
}
}
return cloneObj;
}

通过apache commons lang3包中的SerializationUtils 的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
// 此方法需要克隆对象实现序列化,本质也是字节流拷贝
User1 cloneUser = (User1)SerializationUtils.clone(user1);
// 看下它的源码 可以看到对应的也是字节流的方式拷贝的
public class SerializationUtils {
public SerializationUtils() {
}

public static <T extends Serializable> T clone(T object) {
if (object == null) {
return null;
} else {
byte[] objectData = serialize(object);
ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
SerializationUtils.ClassLoaderAwareObjectInputStream in = null;

Serializable var5;
try {
in = new SerializationUtils.ClassLoaderAwareObjectInputStream(bais, object.getClass().getClassLoader());
T readObject = (Serializable)in.readObject();
var5 = readObject;
} catch (ClassNotFoundException var14) {
throw new SerializationException("ClassNotFoundException while reading cloned object data", var14);
} catch (IOException var15) {
throw new SerializationException("IOException while reading cloned object data", var15);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException var16) {
throw new SerializationException("IOException on closing cloned object data InputStream.", var16);
}

}
return var5;
}
}
// 其他代码略
}

通过Gson 等工具类

1
2
3
Gson gson = new Gson();
User1 cloneUser = gson.fromJson(gson.toJson(User1),User1.class);
// 使用JSON 工具类会将对象转化为字符串,再从字符串转为为新的对象,因为新的对象是从字符串转化而来的,因此不会和原型对象有任何的关联,所以这样也就间接的实现了深克隆;

面试题:Arrays.copyOf()是深克隆还是浅克隆?

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
int [] ages = new int[] {1,2,3,4};
int[] ints = Arrays.copyOf(ages, 2);//输出[1,2]
// 我们还是来看源码
/**
* Copies the specified array, truncating or padding with zeros (if necessary)
* so the copy has the specified length. For all indices that are
* valid in both the original array and the copy, the two arrays will
* contain identical values. For any indices that are valid in the
* copy but not the original, the copy will contain <tt>0</tt>.
* Such indices will exist if and only if the specified length
* is greater than that of the original array.
*
* @param original the array to be copied
* @param newLength the length of the copy to be returned
* @return a copy of the original array, truncated or padded with zeros
* to obtain the specified length
* @throws NegativeArraySizeException if <tt>newLength</tt> is negative
* @throws NullPointerException if <tt>original</tt> is null
* @since 1.6
*/
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
// 从源码 我们可以看到 Arrays.copyOf()最终调用的是native本地方法栈的方法,我们知道本地方法都是直接操作内存的,那么源对象变了,因为都是指向同一个内存地址,所以拷贝对象肯定跟着变,所以此种方法也是浅拷贝;

总结:其实深克隆和浅克隆的应用最常见的还是,对象的转换上;例如我们在操作dao层的时候,实际上更多是映射到实体上,然后我们需要自己封装一些属性返回给前台,这时候我们需要将属性拷贝到对应得DTO上,所以此时的就用到对象拷贝了;那么了解了深浅克隆后,平时的工作写代码时更要注意了;

  • Title: 由一个bug谈谈深浅克隆
  • Author: viEcho
  • Created at : 2021-04-23 19:55:09
  • Updated at : 2024-01-18 14:55:03
  • Link: https://viecho.github.io/2021/0423/deep-shallow-clone.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
由一个bug谈谈深浅克隆