为什么需要克隆对象?直接new一个对象不行吗?
答案是:克隆的对象可以直接使用已经有的属性值,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来克隆对象所用的clone()方法是一个native方法,就是快啊,在底层实现的。而且,通过clone方法赋值的对象跟原来的对象也是相互独立的。
clone()方法
假如说你想复制一个简单变量。很简单:
int apples = 5;
int pears = apples;
不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。
但是如果你复制的是一个对象,情况就有些复杂了。注意,下方这样不是对象的克隆:
Student stu1 = new Student();
Student stu2 = stu1;
这个只是将对象的引用赋值给了另外一个变量。stu2指向的仍然是原来的对象。
那么,怎样才能复制一个对象呢?一种常见的方式就是使用Object类中的clone()方法,该方法源码如下
/*
Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
The general intent is that, for any object x, the expression:
1) x.clone() != x will be true
2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
3) x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object clone() throws CloneNotSupportedException;
可以发现这是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想直接访问到底层与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。
浅克隆和深克隆
1、浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
在Java语言中,通过实现Cloneable接口并重写Object类中的clone()方法可以实现浅克隆。
2、深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象引用所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
(如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。)
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
浅克隆的具体实现
浅克隆的一般步骤是:
1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)
2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
}
}
注意:由于继承下来的clone是protected权限,如果不重写clone方法,并且将clone方法的权限等级上升到public的话,那么clone方法只能在子类调用,不能被外部类调用。
深克隆的具体实现
我们在学生类里再加一个Address类。
1 class Address {
2 private String add;
3
4 public String getAdd() {
5 return add;
6 }
7
8 public void setAdd(String add) {
9 this.add = add;
10 }
11
12 }
13
14 class Student implements Cloneable{
15 private int number;
16
17 private Address addr;
18
19 public Address getAddr() {
20 return addr;
21 }
22
23 public void setAddr(Address addr) {
24 this.addr = addr;
25 }
26
27 public int getNumber() {
28 return number;
29 }
30
31 public void setNumber(int number) {
32 this.number = number;
33 }
34
35 @Override
36 public Object clone() {
37 Student stu = null;
38 try{
39 stu = (Student)super.clone();
40 }catch(CloneNotSupportedException e) {
41 e.printStackTrace();
42 }
43 return stu;
44 }
45 }
46 public class Test {
47
48 public static void main(String args[]) {
49
50 Address addr = new Address();
51 addr.setAdd("杭州市");
52 Student stu1 = new Student();
53 stu1.setNumber(123);
54 stu1.setAddr(addr);
55
56 Student stu2 = (Student)stu1.clone();
57
58 System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
59 System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
60 }
61 }
这样就行了吗?如果我们在main方法中试着改变addr实例的地址比如addr.setAdd("西湖区"),那么就会发现两个学生对象的地址都改变了。原因是浅复制只是复制了addr变量的引用,并没有为克隆来的引用对象单独开辟一块内存区域。所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:
1 package abc;
2
3 class Address implements Cloneable {
4 private String add;
5
6 public String getAdd() {
7 return add;
8 }
9
10 public void setAdd(String add) {
11 this.add = add;
12 }
13
14 @Override
15 public Object clone() {
16 Address addr = null;
17 try{
18 addr = (Address)super.clone();
19 }catch(CloneNotSupportedException e) {
20 e.printStackTrace();
21 }
22 return addr;
23 }
24 }
25
26 class Student implements Cloneable{
27 private int number;
28
29 private Address addr;
30
31 public Address getAddr() {
32 return addr;
33 }
34
35 public void setAddr(Address addr) {
36 this.addr = addr;
37 }
38
39 public int getNumber() {
40 return number;
41 }
42
43 public void setNumber(int number) {
44 this.number = number;
45 }
46
47 @Override
48 public Object clone() {
49 Student stu = null;
50 try{
51 stu = (Student)super.clone(); //浅复制
52 }catch(CloneNotSupportedException e) {
53 e.printStackTrace();
54 }
55 stu.addr = (Address)addr.clone(); //深度复制
56 return stu;
57 }
58 }
59 public class Test {
60
61 public static void main(String args[]) {
62
63 Address addr = new Address();
64 addr.setAdd("杭州市");
65 Student stu1 = new Student();
66 stu1.setNumber(123);
67 stu1.setAddr(addr);
68
69 Student stu2 = (Student)stu1.clone();
70
71 System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
72 System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
73
74 addr.setAdd("西湖区");
75
76 System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
77 System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
78 }
79 }
这样结果就符合我们的想法了。最后我们可以看看API里其中一个实现了clone方法的类:java.util.Date:
/**
* Return a copy of this object.
*/
public Object clone() {
Date d = null;
try {
d = (Date)super.clone();
if (cdate != null) {
d.cdate = (BaseCalendar.Date) cdate.clone();
}
} catch (CloneNotSupportedException e) {} // Won't happen
return d;
}
另外序列化与发序列化实现克隆的方式详见IO基本使用。