Clone详解

2018-03-31  本文已影响152人  Hypercube

1.正确的克隆对象

当需要拷贝一个对象时,很多人建议不使用Java本身的clone方法,理由之一是:正确的实现clone不太容易。的确如此,正确的实现对象的clone,有以下几个步骤:

  1. 待Clone的对象需要实现Cloneable接口。
  2. 覆盖Objectprotected Object clone()方法为待Clone对象的public Object clone()方法。
  3. 待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在互联网发展时期,遇到了某些安全性问题,一些对象并不希望能被克隆(比如用户的密码),由此,将Objectclone()方法的权限由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克隆的精确定义需要满足以下三个条件:

  1. x.clone() != x必为真
  2. 一般情况,x.clone().getClass() == x.getClass()为真
  3. 一般情况,x.clone().equals(x)为真

如果不遵守约定调用super.clone(),那么将会违背第二个条件,使得克隆出的对象与原对象不属于同一个类型。

5.其他的解决方案

由于JAVA的clone()方法在深拷贝方面有诸多缺陷,涌现出了许多解决方案:

  1. Copy Constructor即提供一个可拷贝对象的构造方法。比如在Window中提供一个如下的构造方法:
    public Window(Window window) {
        this.width = window.width;
        this.height = window.height;
    }
  1. 序列化一个对象之后再反序列化。比如先将对象转换为JSON字符串,然后在反序列化得到新对象。Kryo的序列化机制克隆速度更快,可以参考Kryo
  2. 使用反射逐字段克隆对象。如Java Deep Cloning Library

如果一个对象中只包含基本数据类型和不可变对象的引用,此种情况 下,深拷贝和浅拷贝的结果一致,那么推荐使用JAVA的clone()解决方案。

附一些关于clone的讨论:

  1. Java Cloning and Types of Cloning (Shallow and Deep) in Details with Example
  2. recommended solution for deep cloning/copying an instance
  3. Java Cloning: Copy Constructors vs. Cloning
上一篇下一篇

猜你喜欢

热点阅读