从一个bug谈谈深浅克隆

从一个bug谈谈深浅克隆

本篇我们来谈谈深浅克隆, 又叫深浅拷贝!

开始之前

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

// --多线程及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方法即可,那么我们就以此来举例说明什么叫深克隆,什么叫浅克隆;

示例代码如下:

@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 方法源码分析

/**
* 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 接口

@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();
    }
}

通过构造器实现深克隆

@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;
    }
}

通过字节流实现深克隆

// 此方法需要克隆对象实现序列化
 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方法

// 此方法需要克隆对象实现序列化,本质也是字节流拷贝
User1 cloneUser = (User1)SerializationUtils.clone(user1);

通过Gson 等工具类

Gson gson = new Gson();
User1 cloneUser = gson.fromJson(gson.toJson(User1),User1.class);

总结:关于深浅克隆的相关知识点,我们就先总结到这儿;关于面试常问的一个知识点Arrays.copyOf()其实是浅克隆!