目录

深浅拷贝

java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的。

如果想要创建一个对象的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态而互不影响,就需要用到java中对象的复制,如原生的clone()方法。

如何进行对象克隆

Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,所以实体类使用克隆的前提是:

  1. 覆盖clone()方法,并将可见性提升为public
  2. 实现Cloneable接口,这是一个标记接口,自身没有方法。

Object中native修饰的clone方法是浅拷贝

深拷贝与浅拷贝

浅拷贝

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

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

深拷贝

在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
  • 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
  • 拷贝相比于浅拷贝速度较慢并且开销较大。
  • 使用clone进行深拷贝,如果被复制对象不是直接继承Object,中间还有其它继承层次,每一层super类都需要实现Cloneable接口并覆盖clone()方法。一句话来说,如果实现完整的深拷贝,需要被复制对象的继承链、引用链上的每一个对象都实现克隆机制。前面的实例还可以接受,如果有N个对象成员,有M层继承关系,就会很麻烦。

其他深拷贝方式

利用序列化实现深拷贝

clone机制不是强类型的限制,比如实现了Cloneable并没有强制继承链上的对象也实现;也没有强制要求覆盖clone()方法。因此编码过程中比较容易忽略其中一个环节,对于复杂的项目排查就是困难了。

要寻找可靠的,简单的方法,序列化就是一种途径。

  • 被复制对象的继承链、引用链上的每一个对象都实现java.io.Serializable接口。这个比较简单,不需要实现任何方法,serialVersionID的要求不强制,对深拷贝来说没毛病。

  • 实现自己的deepClone方法,将this写入流,再读出来。俗称:冷冻-解冻。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    @Data
    public class Person implements Serializable {
        private String name;
        private Integer age;
        private Address address;
        public Person deepClone() {
            // 序列化
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            // 反序列化
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return ois.readObject();
        }
    }
    

利用Commons-BeanUtils复制对象

maven依赖

1
2
3
4
5
<dependency>
  <groupId>commons-beanutils</groupId>
  <artifactId>commons-beanutils</artifactId>
  <version>1.9.3</version>
</dependency>

使用

1
Person p2=(Person) BeanUtils.cloneBean(p1);

利用cglib复制对象

maven依赖

1
2
3
4
5
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.4</version>
</dependency>

使用

浅拷贝

1
2
3
BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, false);
Person p2=new Person();
beanCopier.copy(p1, p2,null);

深拷贝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, true);
  Person p2=new Person();
  beanCopier.copy(p1, p2, new Converter(){
    @Override
    public Object convert(Object value, Class target, Object context) {
      if(target.isSynthetic()){
        BeanCopier.create(target, target, true).copy(value, value, this);
      }
      return value;
    }
  });

除上述以外还有Dozer、Orika等深拷贝方式。以下是1w次深拷贝各种方式消耗的毫秒数

1
2
3
4
5
6
//dozer:721
//commons-beanutils:229
//cglib:133
//serializable:687
//orika:83
//clone:8

深拷贝的使用总结

  • 原生的clone效率无疑是最高的
  • 偶尔用一次,用哪个都问题都不大
  • 一般性能要求稍高的应用场景,cglib和orika完全可以接受
  • 另外一个考虑的因素,如果项目已经引入了某个依赖,就用那个依赖来做吧,没必要再引入一个第三方依赖