什么是克隆
在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在 Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
克隆的方法使用
Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。
protected native Object clone() throws CloneNotSupportedException;
仔细 观察一下Object类的clone()一个native方法,native方法的效率一般来说都是远高于java中的非native方法。这也解释了为 什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能。对于第二点,也要 观察Object类中的clone()还是一个protected属性的方法。这也意味着如果要应用clone()方法,重写该方法,否则无法调用该clone方法
public class Test213 implements Cloneable {
private int age = 10;
public static void main(String[] args) {
Test213 t =new Test213();
Test213 t1 = null;
try {
t1 = (Test213) t.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(t.equals(t1));//false
System.out.println(t==t1);//false
System.out.println(t1.age);//10
}
}
上面的代码可以证实上面的两点。
Cloneable接口
- 此类实现了 Cloneable 接口,可以调用 Object.clone() 方法可以合法地对该类实例进行按字段复制。
- 如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。
- 实现此接口的类应该使用公共方法重写 Object.clone。
深拷贝与浅拷贝
浅拷贝是拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
浅拷贝的示意图和实现代码
package com.example.test;
public class Professer implements Cloneable{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
package com.example.test;
public class Student1 implements Cloneable {
private String name;
private int age;
private Professer p;
public Professer getP() {
return p;
}
public void setP(Professer p) {
this.p = p;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
package com.example.test;
public class Test214 {
public static void main(String[] args) {
Professer p = new Professer();
p.setAge(50);
p.setName("laohe");
Student1 s = new Student1();
s.setName("laoqiang");
s.setAge(21);
s.setP(p);
Student1 s1 = null;
try {
s1 = (Student1) s.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
s1.setAge(27);
s1.setName("laoqiang1");
s1.setAge(27);
s1.setName("laoqiang1");
s1.getP().setName("laohe1");
s1.getP().setAge(55);
System.out.println(s1.getP().getName()+" "+s.getP().getName());
System.out.println(s1.getAge()+" "+s.getAge());
}
}
运行结果:
我们可以从运行结果:看出age,拷贝的是值,所以前后两次操作,是互不影响的;看出name指向拷贝地址,实际上两种还是指向同一个地址,所以拷贝前后对象修改操作,后面的会覆盖前面的,这种是不好的。为解决相互的影响,我们就有了深拷贝。
构造函数是属于浅拷贝哦,这点一定要记住哦。
深拷贝的示意图和实现代码
可以看出name所指的地址不是一个地址,那么也就是说在复制的时候,也复制了引用变量的值。
修改上述的代码:在Student1.class中
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
Student1 s = (Student1) super.clone();
s.p = (Professer) p.clone();//单独在进行引用变量的赋值
return s;
}
序列化对象
当我们了解了深拷贝之后,发现如果该属性没有引用类型的也罢,有引用类型拷贝,必须都要单独提出来克隆,这样就很繁琐,由此就有了序列化拷贝对象。
首先把实体对象类一定要实现序列化接口,否则不好序列化,其次在需要拷贝的对象中,提供一个拷贝方法:
public Student1 deepClone() throws IOException, ClassNotFoundException{
ObjectOutputStream oos = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ObjectInputStream ois = null;
ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
return (Student1) ois.readObject();
}
要想序列化对象,必须先创建一个OutputStream,然后把它嵌入ObjectOutputStream。这时就能用writeObject()方法把对象写入OutputStream。读的时候需要把InputStream嵌到ObjectInputStream中,然后再调用readObject()方法。
好处:对象序列化不仅能保存对象的副本,而且会跟着对象中的reference把它所引用的对象也保存起来,然后再继续跟踪那些对象的reference,以此类推。这种情形常被称作”单个对象所联结的‘对象网’ “。
缺点:也是耗时呀。
参考学习: