我们知道一个类实现了Cloneable接口就表示它具备了拷贝的能力,如果再覆写clone方法就完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快的多,特别在大对象生成上,这会使性能提升非常显著.但是对象拷贝也有一个比较容易比较忽略的问题:浅拷贝也叫影子拷贝存在对象属性拷贝不彻底的问题。
1.浅拷贝
看如下代码:
public class Client {
public static void main(String[] args) {
Person p =new Person("父亲");
Person s1 = new Person("大儿子",p);
Person s2 = s1.clone();
s2.setName("小儿子");
System.out.println(s1.getName()+" 的父亲是 "+ s1.getFather() .getName());
System.out.println(s2.getName()+" 的父亲是 "+ s2.getFather() .getName());
}
}
public class Person implements Cloneable {
private String name;
private Person father;
public Person(String name) {
super();
this.name = name;
}
public Person(String name, Person father) {
super();
this.name = name;
this.father = father;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person getFather() {
return father;
}
public void setFather(Person father) {
this.father = father;
}
@Override
protected Person clone() throws CloneNotSupportedException {
Person p = null;
try {
p = (Person) p.clone();
} catch (Exception e) {
e.printStackTrace();
}
return p;
}
}
程序中,我们描述了一个这样的场景:一个父亲有两个儿子,大小儿子所以同根同种,所以小儿子对象就通过拷贝大儿子对象来生成,运行输出结果如下:
大儿子 的父亲是 父亲
小儿子 的父亲是 父亲
这样没毛病!
突然有一天,父亲心血来潮想让大儿子认个干爹,也就是大儿子的父亲名称需要重新设置下
public class Client {
public static void main(String[] args) {
Person p =new Person("父亲");
Person s1 = new Person("大儿子",p);
Person s2 = s1.clone();
s2.setName("小儿子");
//大儿子认个干爹
s1.getFather().setName("干爹");
System.out.println(s1.getName()+" 的父亲是 "+ s1.getFather() .getName());
System.out.println(s2.getName()+" 的父亲是 "+ s2.getFather() .getName());
}
}
运行结果:
大儿子 的父亲是 干爹
小儿子 的父亲是 干爹
看输出结果,小儿子父亲也变成干爹了。
出现这个问题的原因是在于clone方法,我们知道所有类都继承Object,Object提供了一个对象拷贝的默认方法,即上面代码中的super.clone()方法,但这个方法是有缺陷的,他提供了一种浅拷贝的方式,也就是说它并不会把对象所有属性全部拷贝一份,而有选择性的拷贝,它的拷贝规则如下:
(1)基本类型
如果变量是基本类型,则拷贝其值,比如int,float等。
(2)对象
如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝的对象与原有对象共享该实例变量,不受访问权限的限制。这在java是很疯狂的,因为它突破了访问权限的定义,一个private修饰的变量,竟可以被两个不同的实例对象访问,这让java访问权限体系情何以堪!
(3)String字符串
这个比较特殊,拷贝的也是一个新地址,是个引用,但是在修改时,他会从字符串池中重新生成新的字符串,原有字符串对象保持不变,在此处我们可以认为String是一个基本类型。
明白了这三个规则,上面的例子就很清晰了,小儿子对象是通过拷贝大儿子产生的,其父亲是同一个人,也就是同一个对象,大儿子修改了父亲名称,小儿子也跟随修改了---------想要正确修改clone方法的代码如下
public Person clone() throws CloneNotSupportedException {
Person p = null;
try {
p = (Person) p.clone();
p.setFather(new Person(p.getFather().getName()));
} catch (Exception e) {
e.printStackTrace();
}
return p;
}
2.浅拷贝
对于浅拷贝的问题,实现了Cloneable接口就具备了拷贝能力,那我们来思考一个问题,如果一个项目有大量的对象通过拷贝生成的我们应该怎么处理?每一个类都谢一个clone方法,并且还要深拷贝?想想工作量就巨大。
有一种更好的方法,可以通过序列化方式来处理,在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中,再从字节流中将其读取出来,这样就可以重建一个对象了,该对象与母对象直接不存在引用共享的问题,也就相当于深拷贝一个对象,代码如下:
public class Person implements Serializable {
/**
*
*/
private static final long serialVersionUID = -2273893604786942028L;
private String name;
private Person father;
public Person(String name) {
super();
this.name = name;
}
public Person(String name, Person father) {
super();
this.name = name;
this.father = father;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person getFather() {
return father;
}
public void setFather(Person father) {
this.father = father;
}
}
public class Client {
public static void main(String[] args) {
Person p =new Person("父亲");
Person s1 = new Person("大儿子",p);
Person s2 = CloneUtils.clone(s1);
s2.setName("小儿子");
s1.getFather().setName("干爹");
System.out.println(s1.getName()+" 的父亲是 "+ s1.getFather() .getName());
System.out.println(s2.getName()+" 的父亲是 "+ s2.getFather() .getName());
}
}