1.正确的克隆对象
当需要拷贝一个对象时,很多人建议不使用Java本身的clone方法,理由之一是:正确的实现clone不太容易。的确如此,正确的实现对象的clone,有以下几个步骤:
- 待Clone的对象需要实现
Cloneable
接口。 - 覆盖
Object
的protected Object clone()
方法为待Clone对象的public Object clone()
方法。 - 待Clone对象及其子类的
clone()
方法里需要调用super.clone()
方法并处理CloneNotSupportedException
异常。
一个clone()
方法的正确实现如下所示:
class Room implements Cloneable{
private String name = "matrix";
private int price = 500;
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// 由于实现了Cloneable接口,那么永不发生
}
return null;
}
}
2. clone存在的问题和原因
重新审视代码,却会发现一些奇怪的地方。
首先,接口Cloneable
只是一个标记接口,其中没有任何方法,但是接口文档表明,如果待Clone对象不实现该接口,就会抛出CloneNotSupportedException
异常。解答该问题,需要深入JDK源码Object的clone()方法,截取以下片段说明:
// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
// 检查对象是否实现了Cloneable接口(数组默认实现Cloneable)
if (!klass->is_cloneable()) {
ResourceMark rm(THREAD);
THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}
// Make shallow object copy
const int size = obj->size();
oop new_obj_oop = NULL;
// 分配空间
if (obj->is_javaArray()) {
const int length = ((arrayOop)obj())->length();
new_obj_oop = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
} else {
new_obj_oop = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
}
// 具体的拷贝过程
Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj_oop,
(size_t)align_object_size(size) / HeapWordsPerLong);
clone()
方法并没有声明在Cloneable
中从而使用Java自有的接口语言特性实现,而是在clone()
方法的底层硬编码建立和接口的联系。没有使用接口语言特性,这是clone()
不好用的一大原因。
其次,Object
类的clone()
方法的访问权限声明为protected
,而待Clone对象需要覆盖声明为public
。一个不可考的原因是:Java在互联网发展时期,遇到了某些安全性问题,一些对象并不希望能被克隆(比如用户的密码),由此,将Object
中clone()
方法的权限由public
降低为protected
,从而使对象默认不具有Clone能力,以便提高安全性。
最后,需要在待Clone
对象中约定调用super.clone()
。原因正是要最终调用Object
中的clone()
方法,以便执行具体的克隆过程。
3. 浅拷贝和深拷贝
明白了这些,感觉很开心,继续扩充代码,在房子里开一扇窗:
class Window implements Cloneable{
private int width = 200;
private int height = 300;
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// never happen
}
return null;
}
}
class Room implements Cloneable{
private String name = "matrix";
private int price = 12;
Window window = new Window();
// clone方法相同省略
}
愉快的克隆一间房子:
public static void main(String[] args) {
Room room = new Room();
Room clone = (Room) room.clone();
System.out.println(room != clone); // true
System.out.println(room.window != clone.window); // false
}
结果却让人失望,克隆出来的新房子和老房子共享了同一扇窗子,这并不是我们希望的。回顾先前clone()
方法的native源码,其中新对象中的字节由老对象拷贝而来,而Window window = new Window()
在Room
中存储的是一个引用,所以拷贝的仅仅是一个引用。更官方的说法是:field by filed copy即按字段拷贝。也许你已经听说过,这种拷贝方式称之为浅拷贝,是JAVA的默认实现方式。与之对应的另一种拷贝方式称之为深拷贝,这种方式会将房子中的窗子也拷贝,所以需要额外的代码实现,由于窗子已经实现Cloneable
,所以仅需在Room
中添加一行代码:
public Object clone() {
try {
Room room = (Room) super.clone();
// 窗子也需要克隆
room.window = (Window) room.window.clone();
return room;
} catch (CloneNotSupportedException e) {
// never happen
}
return null;
}
再次克隆一间房子,运行结果如下,终于不用担心邻居关闭自家的窗户了。
true
true
4.clone的精确含义
骨傲天是个我行我素的人,凭什么要遵守约定调用super.clone()
呢?于是他使用魔法准备克隆一间教室:
// 普通房间改造的教室,里面空空如也
class ClassRoom extends Room {
}
class Room implements Cloneable{
private String name = "matrix";
private int price = 12;
Window window = new Window();
public Object clone() {
Room room = new Room();
room.window = new Window();
return room;
}
}
克隆开始:
public static void main(String[] args) {
Room classRoom = new ClassRoom();
Room cloneClass = (Room) classRoom.clone();
System.out.println(classRoom != cloneClass);
System.out.println(classRoom.window != cloneClass.window);
System.out.println(classRoom.getClass());
System.out.println(cloneClass.getClass());
}
克隆的结果:
true
true
class clone.ClassRoom
class clone.Room
开始地很高兴,结束地很伤心,克隆出的根本不是教室,而是老房子。这不是一次成功的克隆,违背了克隆的定义。而JAVA克隆的精确定义需要满足以下三个条件:
-
x.clone() != x
必为真 - 一般情况,
x.clone().getClass() == x.getClass()
为真 - 一般情况,
x.clone().equals(x)
为真
如果不遵守约定调用super.clone()
,那么将会违背第二个条件,使得克隆出的对象与原对象不属于同一个类型。
5.其他的解决方案
由于JAVA的clone()
方法在深拷贝方面有诸多缺陷,涌现出了许多解决方案:
- Copy Constructor即提供一个可拷贝对象的构造方法。比如在
Window
中提供一个如下的构造方法:
public Window(Window window) {
this.width = window.width;
this.height = window.height;
}
- 序列化一个对象之后再反序列化。比如先将对象转换为JSON字符串,然后在反序列化得到新对象。Kryo的序列化机制克隆速度更快,可以参考Kryo。
- 使用反射逐字段克隆对象。如Java Deep Cloning Library。
如果一个对象中只包含基本数据类型和不可变对象的引用,此种情况 下,深拷贝和浅拷贝的结果一致,那么推荐使用JAVA的clone()
解决方案。
附一些关于clone的讨论:
网友评论