Java中的深拷贝,浅拷贝是一个很重要的概念,之前也曾因为这个问题在实际一个需求中翻过车,今天就把这个拿出来说一下,以下是这篇博客的大纲
一.引入
二.深拷贝&浅拷贝
三.clone方法&Cloneable接口
四.需要注意的问题
五.总结
一.引入
在以前有过这种因为浅拷贝出来的对象,改变了这个浅拷贝对象的某些属性值,导致原对象的属性也改变。 这其中就涉及到了Java中的深拷贝与浅拷贝问题。
修改步骤是
1. 让该类实现java.lang.Cloneable接口;
2. 重写(override)Object类的clone()方法。
二.深拷贝&浅拷贝
首先需要明白,深拷贝和浅拷贝都是针对一个已有对象的操作。
(1)浅克隆(shallow clone)
浅拷贝是指拷贝对象时仅仅拷贝对象本身和对象中的基本变量,而不拷贝对象包含的引用指向的对象。
(2)深克隆(deep clone)
深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
举例区别一下:对象CompanyA中包含对TeamA的引用,TeamA中包含对EmployeeA的引用。浅拷贝CompanyA得到CompanyB,CompanyB中依然包含对TeamA的引用,TeamA中依然包含对EmployeeA的引用。深拷贝则是对浅拷贝的递归,深拷贝CompanyA得到CompanyB,CompanyB中包含对TeamB(TeamA的copy)的引用,TeamB中包含对EmployeeB(EmployeeA的copy)的引用。
下面画个图看起来可能会更直观一点
三.clone方法&Cloneable接口
Cloneable和Serializable一样都是标记型接口,它们内部都没有方法和属性,implements Cloneable表示该对象能被克隆,能使用Object.clone()方法。如果没有implements Cloneable的类调用Object.clone()方法就会抛出CloneNotSupportedException。
一个典型的调用clone()方法的示例如下
class CloneClass implements Cloneable{
public int param;
@Override
public Object clone(){
CloneClass o = null;
try{
o = (CloneClass)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
四.需要注意的问题
1.多层拷贝,多层克隆问题:
深拷贝的时候如果需要深拷贝的对象里面的属性如果都是基本类型,直接使用上面的clone方法示例即可,但是如果需要进行深拷贝的对象的属性有其他的引用类型的时候。需要将其他的这些引用类型的属性也进行深拷贝,即还要重写这些引用类型的属性的clone方法,然后再需要深拷贝的类的clone方法中调用。下面举个栗子来示范一下:
package com.dahua.psis.client.common.params;
import java.util.Date;
/**
* TODO
*
* @author 31777
* @version Ver 0.1
* @since Ver 0.1
* @Date 2018年1月14日
*
*/
public class Child implements Cloneable {
private String name;
private Date birth;//Date里面已经重写了clone()方法
private Double weight;
public Child() {
}
public Child(String name, Date birth, Double weight) {
this.name = name;
this.birth = birth;
this.weight = weight;
}
/**
* Return a copy of this object.
*/
@Override
public Child clone() {
Child c = null;
try {
c = (Child) super.clone();
//在Child中重写Child的clone方法
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return c;
}
.............getter setter methods
@Override
public String toString() {
return "Child [name=" + name + ", birth=" + birth + ", weight=" + weight + "]";
}
public static void main(String[] args) {
Child childA = new Child("aa", new Date(), 11.1);
Child childB = childA.clone();
childB.setName("zzzzz");
System.out.println(childA.toString() + "---" + childB.toString());
}
}
public class Father implements Cloneable {
private String name;
private Integer age;
private Child child;
/**
* Return a copy of this object.
*/
@Override
public Father clone() {
Father f = null;
try {
f = (Father) super.clone();
//1.在Child中重写Child的clone方法;2.在对应引用类型属性中重写clone方法并在此处进行引用
f.child = f.getChild().clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
public Father() {
}
public Father(String name, Integer age) {
this.name = name;
this.age = age;
}
.............getter setter methods
@Override
public String toString() {
return "Father [name=" + name + ", age=" + age + ", child=" + child.toString() + "]";
}
public static void main(String[] args) {
Child child = new Child("joe", new Date(), 11.3);
Father fatherA = new Father("dave", 29);
fatherA.setChild(child);
System.out.println(fatherA.toString());
Father fatherB = fatherA.clone();
fatherB.setAge(33);
System.out.println("只改变Father类的基本类型属性的值");
System.out.println(fatherB.toString() + "---" + fatherA.toString());
System.out.println("同时改变Father类的Child相关的属性的值");
fatherB.getChild().setName("NEW-NAME");
System.out.println(fatherB.toString() + "---" + fatherA.toString());
/*
**********************这里是只在Father中重写clone方法后打印的*****************************************
Father [name=dave, age=29, children=Child [name=joe, birth=Sun Jan 14 13:19:33 CST 2018, weight=11.3]] 只改变Father类的基本类型属性的值 Father [name=dave, age=33, children=Child [name=joe, birth=Sun Jan 14 13:19:33 CST 2018, weight=11.3]]---Father [name=dave, age=29, children=Child [name=joe, birth=Sun Jan 14 13:19:33 CST 2018, weight=11.3]] 同时改变Father类的Child相关的属性的值 Father [name=dave, age=33, children=Child [name=NEW-NAME, birth=Sun Jan 14 13:19:33 CST 2018, weight=11.3]]---Father [name=dave, age=29, children=Child [name=NEW-NAME, birth=Sun Jan 14 13:19:33 CST 2018, weight=11.3]] *//******************这里是在两个类中都重写clone方法并且在Father的clone方法中还调用了Child属性的clone方法后打印的************** * Father [name=dave, age=29, child=Child [name=joe, birth=Sun Jan 14 13:34:17 CST 2018, weight=11.3]] 只改变Father类的基本类型属性的值 Father [name=dave, age=33, child=Child [name=joe, birth=Sun Jan 14 13:34:17 CST 2018, weight=11.3]]---Father [name=dave, age=29, child=Child [name=joe, birth=Sun Jan 14 13:34:17 CST 2018, weight=11.3]] 同时改变Father类的Child相关的属性的值 Father [name=dave, age=33, child=Child [name=NEW-NAME, birth=Sun Jan 14 13:34:17 CST 2018, weight=11.3]]---Father [name=dave, age=29, child=Child [name=joe, birth=Sun Jan 14 13:34:17 CST 2018, weight=11.3]] */}}
2.在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
如果一个类重写了Object的clone()方法,同时需要继承Cloneable接口(虽然这个接口没有定义clone()方法),否则在调用clone()时候会报错CloneNotSupportedException异常,也就是说Cloneable接口只是一个合法调用clone()的标识(mark-interface)。
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦,而且如果修改了类的结构还需要重修修改clone方法,这时我们可以用序列化的方式来实现对象的深拷贝。可以使用Serializable运用反序列化手段,调用java.io.ObjectInputStream对象的 readObject()方法。如下给出一个工具类。
public class CloneUtils {
// 拷贝一个对象
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
// 拷贝产生的对象
T clonedObj = null;
try {
// 读取对象字节数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
// 分配内存空间,写入原始对象,生成新对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
//返回新对象,并做类型转换
clonedObj = (T)ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return clonedObj;
}
}
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。这样就比较麻烦了。
简单的做法,实现Apache Commons可以直接实现
• 深克隆/拷贝(deep clone/copy): 即使用Apache下的commons工具包中的SerializationUtils
• 浅克隆/拷贝(shallow clone/copy):BeanUtils
简单的克隆,也可以通过 copy-constructor (拷贝构造方法) 实现,详情参考 Effective Java 作者 Josh Bloch 的这篇访谈: Copy Constructor Versus Cloning 其中也谈到了 Cloneable 接口的很多缺点。
五.总结
1.clone说明
如果一个对象内部只有基本数据类型,那用 clone() 方法获取到的就是这个对象的深拷贝,而如果其内部还有引用数据类型,那用 clone() 方法就是一次浅拷贝的操作,除非对其他引用类型也重写clone并在上级clone方法内进行调用。
2.实现对象克隆有两种方式:
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
3.应用:
选择使用clone方法还是使用序列化工具类,其实查看源码说明
/**
* <p>Deep clone an <code>Object</code> using serialization.</p>
*
* <p>This is many times slower than writing clone methods by hand
* on all objects in your object graph. However, for complex object
* graphs, or for those that don't support deep cloning this can
* be a simple alternative implementation. Of course all the objects
* must be <code>Serializable</code>.</p>
*
* @param object the <code>Serializable</code> object to clone
* @return the cloned object
* @throws SerializationException (runtime) if the serialization fails
*/
public static Object clone(Serializable object) {
return deserialize(serialize(object));
}
使用序列化的性能是不如自己手写clone方法的,对于大部分的实际引用场景,需要我们做深拷贝操作的对象的属性基本都是比较简单的,这个时候我建议还是直接手写clone方法,而对于过分复杂,属性有很多引用类型并且会经常做调整的可以使用SerializationUtils。