美文网首页
Clone详解

Clone详解

作者: Hypercube | 来源:发表于2018-03-31 00:44 被阅读152次

    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

    相关文章

      网友评论

          本文标题:Clone详解

          本文链接:https://www.haomeiwen.com/subject/wfiwcftx.html