深浅拷贝
目录
java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的。
如果想要创建一个对象的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态而互不影响,就需要用到java中对象的复制,如原生的clone()方法。
如何进行对象克隆
Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,所以实体类使用克隆的前提是:
- 覆盖clone()方法,并将可见性提升为public
- 实现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依赖
|
|
使用
|
|
利用cglib复制对象
maven依赖
|
|
使用
浅拷贝
|
|
深拷贝
|
|
除上述以外还有Dozer、Orika等深拷贝方式。以下是1w次深拷贝各种方式消耗的毫秒数
|
|
深拷贝的使用总结
- 原生的clone效率无疑是最高的
- 偶尔用一次,用哪个都问题都不大
- 一般性能要求稍高的应用场景,cglib和orika完全可以接受
- 另外一个考虑的因素,如果项目已经引入了某个依赖,就用那个依赖来做吧,没必要再引入一个第三方依赖