浅谈Java中的克隆机制

Posted 那些年的代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Java中的克隆机制相关的知识,希望对你有一定的参考价值。

从JDK1.0版本开始,Java语言就提供了克隆机制。看到“克隆”二字,我们可能会有一些疑问。

  1. 克隆针对的是类还是对象?
  2. Java如何实现克隆?

“克隆”二字对于我们并不陌生,克隆就是进行复制。现实生活中也有很多克隆的案例,比如克隆鱼、克隆羊等。从面向对象的角度来看,鱼、羊就是我们抽象出来的类,克隆鱼、克隆羊就是类的实例。所以,Java语言中克隆针对的是类的实例。克隆羊、克隆鱼都是基于细胞实现的,那么Java中的克隆如何实现呢?莫慌,Cloneable接口告诉了我们答案。

/**
 1. A class implements the <code>Cloneable</code> interface to
 2. indicate to the {@link java.lang.Object#clone()} method that it
 3. is legal for that method to make a
 4. field-for-field copy of instances of that class.
 5. <p>
 6. 一个实现了Cloneable类意味着可以通过java.lang.Object的clone()合法的对该类的实例的属性逐一复制。
 7.  8. Invoking Object‘s clone method on an instance that does not implement the
 8. <code>Cloneable</code> interface results in the exception
 9. <code>CloneNotSupportedException</code> being thrown.
 10. <p>
 11. 对一个没有实现Cloneable接口的类的实例调用clone()会抛出CloneNotSupportedException。
 12.  14. By convention, classes that implement this interface should override
 13. <tt>Object.clone</tt> (which is protected) with a public method.
 14. See {@link java.lang.Object#clone()} for details on overriding this
 15. method.
 16. 按照惯例,实现了此接口的类应该重写clone(),重写时将该方法由受保护变为公开。
 17. 
 18. Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 19. Therefore, it is not possible to clone an object merely by virtue of the
 20. fact that it implements this interface.  Even if the clone method is invoked
 21. reflectively, there is no guarantee that it will succeed.
 22. 需要注意的是,此接口并不包含clone()。因此,仅依靠实现此接口是不可能实现对象克隆的。即使clone()被成功调用,也不能保证克隆可以成功。
 23. @author  unascribed
 24. @see     java.lang.CloneNotSupportedException
 25. @see     java.lang.Object#clone()
 26. @since   JDK1.0
 */
public interface Cloneable {
}

 

通过阅读Cloneable接口的注释,可以得知Java实现克隆需要遵循以下规则:

  1. 必须实现Cloneable接口
  2. 实现Cloneable的类应该重写clone(),重写时该方法的修饰符为public。

实际上,克隆调用的是Object类的clone()方法,clone()是一个本地方法,默认的修饰符是protected。

protected native Object clone() throws CloneNotSupportedException;

下面,我们来通过代码来检验下克隆机制。这里定义一个学生类Stundent,班级类Classes和测试类TestClone。学生类有三个属性,引用类型name、基本类型age和引用类型classes,并实现了Cloneable接口。

/**
 1. 学生类
 2. @author zhaoheng
 3. @date 2018年8月23日
 */
public class Student implements Cloneable{

    private String name;//引用类型

    private int age;//基本类型

    private Classes classes;//引用类型

    //这里省略gettters and setters

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + ", classes=" + classes + "]";
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student stu = (Student)super.clone();
        return stu;
    }   
}

班级类Classes有两个属性,基本类型classId和引用类型className,没有实现Cloneable接口。

/**
 4. 班级类
 5. @author zhaoheng
 6. @date 2018年8月23日
 */
public class Classes{

    private int classId;//基本类型

    private String className;//引用类型

    //这里省略gettters and setters

    @Override
    public String toString() {
        return "Classes [classId=" + classId + ", className=" + className + "]";
    }
}

 

在测试类TestClone 中,采用以下步骤检验:

  1. 先创建一个Student类的实例stu,然后通过对stu实例的克隆得到stu2实例。
  2. 对原始实例stu和克隆实例stu2的属性进行比较。
  3. 修改克隆实例stu2的属性。
  4. 验证原始实例stu的属性是否受到影响。
/**
 * 测试类
 * @author zhaoheng
 * @date 2018年8月23日
 */
public class TestClone {

    public static void main(String[] args) {
        Student stu = new Student();
        stu.setName("张三");
        stu.setAge(10);

        Classes classes = new Classes();
        classes.setClassId(101);
        classes.setClassName("一班");
        stu.setClasses(classes);

        try {
            System.out.println("浅克隆测试------");
            //克隆
            Student stu2 = (Student)stu.clone();
            System.out.println("两个对象是否相同:" + (stu == stu2));
            System.out.println("两个对象的name属性是否相同:" + (stu.getName() == stu2.getName()));
            System.out.println("两个对象的classes属性是否相同:" + (stu.getClasses() == stu2.getClasses()));
            System.out.println("浅克隆,Stu " + stu);
            System.out.println("浅克隆,Stu2 " + stu);

            System.out.println("修改克隆对象属性");

            stu2.setName("李四");//修改姓名
            stu2.setAge(20);//修改年龄
            stu2.getClasses().setClassId(102);//修改班级编号
            stu2.getClasses().setClassName("二班");//修改班级名称
            System.out.println("修改克隆对象属性后,Stu " + stu);
            System.out.println("修改克隆对象属性后,Stu2 " + stu2);

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

Debug测试类,分别观察原始实例stu和克隆实例stu2的属性。 
技术图片 
技术图片

运行测试类,得到如下结果:

浅克隆测试------
两个对象是否相同:false
两个对象的name属性是否相同:true
两个对象的classes属性是否相同:true
浅克隆,Stu Student [name=张三, age=10, classes=Classes [classId=101, className=一班]]
浅克隆,Stu2 Student [name=张三, age=10, classes=Classes [classId=101, className=一班]]
修改克隆对象属性
修改克隆对象属性后,Stu Student [name=张三, age=10, classes=Classes [classId=102, className=二班]]
修改克隆对象属性后,Stu2 Student [name=李四, age=20, classes=Classes [classId=102, className=二班]]

 

通过Debug观察属性和运行测试类得到的结果,可以发现原始对象stu和克隆对象stu2不是同一对象,但是两个对象的name属性和classes属性是同一个对象,并且原始对象stu和克隆对象stu2的属性值是相等的,这也验证了“Java的克隆机制是对类的实例的属性逐一复制”。name属性和classes同为引用类型的实例,克隆后原始对象stu和克隆对象stu2的name属性和classes属性是同一对象,说明没有实现Cloneable接口的类的实例克隆是通过传引用实现的。

修改了克隆对象stu2的各个属性后,可以发现:原始对象stu的classes属性跟随克隆对象stu2的classes属性变化了,但name和age属性没有跟随变化,这是为什么呢?

age属性没有跟随变化比较容易理解,因为age属性是int类型,基本类型克隆时只是传值,不存在传引用。

同样是传引用,为什么name属性没有跟随变化,难道name属性有特别之处?name属性是String类型,String类型也没有实现Conleable啊,很是困惑。

回忆下String类的声明,public final class String implements …,final关键字是不是很可疑呢?再想想final关键字的作用,一是不可变,二是禁止指令重排。不可变,不可变,是不是想到什么了?正是因为String不可变的特性,克隆对象stu2并没有修改name属性的值,而是修改了name属性的引用。这就是为什么name属性没有跟随克隆对象的变化而变化,String不可变好像让浅克隆变为了深克隆。

但是,期望原始对象stu的classes属性不要跟随克隆对象stu2的属性变化而变化,那自然是让Classes类实现Cloneable接口并重写clone()。那就来修改一波代码吧。。。

Student类主要修改clone(),修改后的代码如下:

/**
 1. 学生类
 2. @author zhaoheng
 3. @date 2018年8月23日
 */
public class Student implements Cloneable{

    private String name;//引用类型

    private int age;//基本类型

    private Classes classes;//引用类型

    //这里省略gettters and setters

    @Override
    public String toString() {
        return "Student [name=" + name + ", age=" + age + ", classes=" + classes + "]";
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student stu = (Student)super.clone();
        //克隆classes属性时调用Classes类的clone()
        Classes cla = (Classes)classes.clone();
        stu.setClasses(cla);
        return stu;
    }   
}

 

Classes类实现Cloneable接口并重写clone(),修改后的代码如下:

/**
 4. 班级类
 5. @author zhaoheng
 6. @date 2018年8月23日
 */
public class Classes implements Cloneable {

    private int classId;//基本类型

    private String className;//引用类型

    //这里省略gettters and setters

    @Override
    public String toString() {
        return "Classes [classId=" + classId + ", className=" + className + "]";
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

 

测试类TestClone修改后的代码如下:

/**
 * 测试类
 * @author zhaoheng
 * @date 2018年8月23日
 */
public class TestClone {

    public static void main(String[] args) {
        Student stu = new Student();
        stu.setName("张三");
        stu.setAge(10);

        Classes classes = new Classes();
        classes.setClassId(101);
        classes.setClassName("一班");
        stu.setClasses(classes);

        try {
            System.out.println("深克隆测试------");
            //克隆
            Student stu2 = (Student)stu.clone();
            System.out.println("两个对象是否相同:" + (stu == stu2));
            System.out.println("两个对象的name属性是否相同:" + (stu.getName() == stu2.getName()));
            System.out.println("两个对象的classes属性是否相同:" + (stu.getClasses() == stu2.getClasses()));
            System.out.println("深克隆,Stu " + stu);
            System.out.println("深克隆,Stu2 " + stu);

            System.out.println("修改克隆对象属性");

            stu2.setName("李四");//修改姓名
            stu2.setAge(20);//修改年龄
            stu2.getClasses().setClassId(102);//修改班级编号
            stu2.getClasses().setClassName("二班");//修改班级名称
            System.out.println("修改克隆对象属性后,Stu " + stu);
            System.out.println("修改克隆对象属性后,Stu2 " + stu2);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

Debug测试类,分别观察原始实例stu和克隆实例stu2的属性。 
技术图片 
技术图片

运行修改后的测试类,结果如下:

深克隆测试------
两个对象是否相同:false
两个对象的name属性是否相同:true
两个对象的classes属性是否相同:false
深克隆,Stu Student [name=张三, age=10, classes=Classes [classId=101, className=一班]]
深克隆,Stu2 Student [name=张三, age=10, classes=Classes [classId=101, className=一班]]
修改克隆对象属性
修改克隆对象属性后,Stu Student [name=张三, age=10, classes=Classes [classId=101, className=一班]]
修改克隆对象属性后,Stu2 Student [name=李四, age=20, classes=Classes [classId=102, className=二班]]

通过Debug观察属性和运行测试类得到的结果,克隆后原始对象stu和克隆对象stu2的classes属性不是同一个对象。修改了克隆对象stu2的各个属性后,可以发现:原始对象stu的属性不再跟随克隆对象stu2的属性变化而变化。

通过对Java克隆机制的学习和检验,我们得出一个结论:如果要对某个类的的实例的引用类型属性实现深克隆,要求引用类型属性对于的类实现Cloneable接口并重写clone()。

至此,本篇文章对Java的克隆机制介绍完毕。 
由于笔主水平有限,笔误或者不当之处还请批评指正。

以上是关于浅谈Java中的克隆机制的主要内容,如果未能解决你的问题,请参考以下文章

Java Object类中的-----------clone()克隆机制

java反射机制浅谈

浅谈浅克隆(shallow clone)和 深克隆(deep clone)

浅谈Java的反射机制

浅谈利用同步机制解决Java中的线程安全问题

GitGit 分支管理 ( 克隆远程分支 | 克隆 master 分支 git clone | 查看远程分支 git branch -a | 克隆远程分支 git checkout -b )(代码片段