JavaSE学习随笔(一) Cloneable接口源码分析与技术细节

一、引言

 

       Cloneable接口是Java开发中常用的一个接口, 它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中,注意,这里所说的“拷贝”拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。

       在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要具备拷贝功能的类实现Cloneable接口,并重写clone()方法,就可以通过调用clone()方法的方式简洁地实现实例拷贝功能。接下来我们将从Cloneable接口的源码入手,对其技术细节和使用方法进行详细的介绍。


二、源码分析


       打开JavaSE源码找到Coloneable接口,其源代码如下。我们会发现一件很有意思的事情,那就是Cloneable接口竟然没有定义任何的接口方法,这是为什么呢?

/*Cloneable接口源代码,JDK1.8*/
public interface Cloneable {
}

       Cloneable接口之所以没有定义任何的接口的原因其实很简单,那就是在Java中,所有类的终极父类已经将clone()方法定义为所有类都应该具有的基本功能,只是将该方法声明为了protected类型。该方法定义了逐字段拷贝实例的操作。它是一个native本地方法,因此没有实现体,而且在拷贝字段时,除了Object类的字段外,其子类的新字段也将被拷贝到新的实例中。

/*Object类中clone()方法的定义*/
protected native Object clone() throws CloneNotSupportedException;

      看到这里,有同学可能会想,既然Object类中既然已经有了一个定义实例拷贝操作的方法,那为什么还是需要让想具备实力拷贝功能的类实现Cloneable接口呢?其实,Cloneable接口在这里起到了一种标识的作用,表明实现它的类具备了实例拷贝功能,在Cloneable接口的官方javadoc文档中有这样一段话:

       "Invoking Object's clone method on an instance that does not implement the Cloneable interface results in the exception CloneNotSupportedException being thrown. JDK1.8"

也就是说,如果一个类不实现该接口就直接调用clone()方法的话,即便已将clone()方法重写为public,那还是会抛出“不支持拷贝”异常。因此,要想使一个类具备拷贝实例的功能,那么除了要重写Object类的clone()方法外,还必须要实现Cloneable接口。下面的代码即可以使一个Cloneableclass具备了浅拷贝实例(关于浅拷贝和深拷贝的含义将会在下一节介绍,在这里读者只需要将它看作普通拷贝即可)的功能。

class CloneableClass implements Cloneable {
    
    /*
     * user code....
     */

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }// clone
 
}/*CloneableClass*/

 

三、浅拷贝与深拷贝


1. 浅拷贝

      前面提到了浅拷贝与深拷贝的概念,这是任何一种面向对象的编程语言中都必须要讨论的内容。在java中,对象创建后需要有一个引用变量来指向该对象实际的地址空间,也就是说引用变量与对象实体是两个不同的数据体。在Object类的clone()方法中,对对象字段进行复制时,如果字段是基本数据类型(如int、double等),则会复制字段的值到一个新的变量中,而字段是引用类型,则仅会将引用值复制给新对象中的相应字段中,也就是说,两个字段指向了同一个对象实例。我们用一个例子来说具体明浅拷贝的性质。

      首先我们先定义一个用户自定义的类Test,该类中有一个字符串类型的成员变量,该类将用于验证浅拷贝的性质。

class Test {
    
    public String userData = null;
    
    public Test(String userData) {
        this.userData = userData;
    }// constructor
    
}/*UserClass*/

       接着,我们在定义一个具有拷贝实例功能的类实现Cloneable接口并按照上文所述的方式重写Clone()方法,即将方法属性重写为public,并在重写方法中调用Object类的本地clone()方法。除此之外,该类还定义了四个字段,其中一个为基本数据类型,其余为引用数据类型。

class CloneableClass implements Cloneable {
    
    public Test         data1 = null;
    public double       data2 = 0;
    public String       data3 = null;
    public StringBuffer data4 = null;
        
    public CloneableClass(Test data1, double data2, String data3, StringBuffer data4) {
        this.data1 = data1;
        this.data2 = data2;
        this.data3 = data3;
        this.data4 = data4;
    }// constructor

    /**
     * 用于显示对象中各字段的值
     */
    public void show() {
        System.out.println("data1 = " + data1.userData);
        System.out.println("data2 = " + data2);
        System.out.println("data3 = " + data3);
        System.out.println("data4 = " + data4);     
    }// show
    
    /**
     * 重写clone()方法为public类型,并调用Object的本地clone()方法,实现浅拷贝功能
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }// clone
 
}/*CloneableClass*/

       接下来是客户端代码,首先创建一个CloneableClass类对象作为初始实例,该实例的各字段值如构造函数所示。然后在调用初始实例的clone()方法创建一个拷贝实例,显示初始实例与拷贝实例各字段的值并判断他们是不是指向了同一个对象实例。

public class CloneableDemo {

    public static void main(String[] args) throws CloneNotSupportedException {
        Test test1 = new Test("original data");
        StringBuffer strBuf = new StringBuffer("origin data");
        
        CloneableClass org = new CloneableClass(test1, 1.0, "original", strBuf);
        CloneableClass copy = null;
        Object objTemp = org.clone();
        if (objTemp instanceof CloneableClass) {
            copy = (CloneableClass)objTemp;
        }// if
        
        System.out.println("copy == original? " + (copy == org));
        System.out.println();
        System.out.println("data of original:");
        org.show();
        System.out.println();
        System.out.println("data of copy:");
        copy.show();

        System.out.println();
        System.out.println("org.data1 == copy.data1? " + (org.data1 == copy.data1));
        System.out.println("org.data2 == copy.data2? " + (org.data2 == copy.data2));
        System.out.println("org.data3 == copy.data3? " + (org.data3 == copy.data3));
        System.out.println("org.data4 == copy.data4? " + (org.data4 == copy.data4));
    }// main

}/*CloneableDemo*/

        执行结果:

JavaSE学习随笔(一) Cloneable接口源码分析与技术细节_第1张图片

       从执行结果上来看copy == org为false说明copy和org是两个不同的对象实例(对于引用类型的比变量,“==”判断的是对象地址是否相同,也就是是不是指向了同一个对象),但是他们字段的值却是相同的。copy和org各字段的“==”判断全为true也说明本地Object本地clone()对实例引用型字段进行的是浅拷贝。

       那么浅拷贝会造成什么样的后果呢?由于浅拷贝仅将字段的引用值复制给了新的字段,但是却并没有创建新的相应对象,也就是说copy和org中的两个字段都指向了同一个对象实例。这样,我们对copy中各字段所指向对象的属性进行了修改,org中的成员对象也会随之改变,客户端代码和运行结果如下所示。

public class CloneableDemo {

    public static void main(String[] args) throws CloneNotSupportedException {
        Test test1 = new Test("original data");
        StringBuffer strBuf = new StringBuffer("origin data");
        
        CloneableClass org = new CloneableClass(test1, 1.0, "original", strBuf);
        CloneableClass copy = null;
        Object objTemp = org.clone();
        if (objTemp instanceof CloneableClass) {
            copy = (CloneableClass)objTemp;
        }// if
        
        // 修改copy中各字段指向对象的属性
        copy.data1.userData = "Copy data";
        copy.data2 = 2.0;
        copy.data3 = "Copy";
        copy.data4.replace(0, copy.data4.length(), "Copy data");

        System.out.println();
        System.out.println("After modify, data of original:");
        org.show();
        System.out.println();
        System.out.println("After modify, data of copy:");
        copy.show();
    }// main

}/*CloneableDemo*/
运行结果:

JavaSE学习随笔(一) Cloneable接口源码分析与技术细节_第2张图片

       从运行结果可以看出,由于CloneableClass的data1和data4字段均为引用型变量,所以经过org浅拷贝出来的copy实例中的data1和data4字段与org中的相应字段实际上指向的是同一个对象,因此,copy对成员对象的修改也就导致了org中相应成员对象的随之改变。需要说明的是,data3虽然也是引用型变量,但由于字符串是常量,所以语句copy.data3 = “Copy”并没有对原字符串进行修改,而是将copy的data3字段指向了一个新的字符串“Copy”。


 2. 深拷贝

       从上文讲的浅拷贝概念与性质中不难退出深拷贝的含义,那就是对于引用型变量,深拷贝会开辟一块新的内存空间,将被复制引用所指向的对象实例的各个属性复制到新的内存空间中,然后将新的引用指向块内存(也就是一个新的实例)。要想实现深拷贝的功能,我们在重写clone()方法的时候,就不能再简单地调用Object的本地clone()方法,而是要对上文中的CloneableClass做如下修改:

class CloneableClass implements Cloneable {
    
    public Test         data1 = null;
    public double       data2 = 0;
    public String       data3 = null;
    public StringBuffer data4 = null;
        
    public CloneableClass(Test data1, double data2, String data3, StringBuffer data4) {
        this.data1 = data1;
        this.data2 = data2;
        this.data3 = data3;
        this.data4 = data4;
    }// constructor

    /**
     * 用于显示对象中各字段的值
     */
    public void show() {
        System.out.println("data1 = " + data1.userData);
        System.out.println("data2 = " + data2);
        System.out.println("data3 = " + data3);
        System.out.println("data4 = " + data4);     
    }// show
    
    /**
     * 重写clone()方法为public类型,对每一个字段都创建一个新的对象,并将原字段指向对象中的属性
     * 复制到新的对象中,从而实现深拷贝功能。
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        Test         data1 = new Test(this.data1.userData);  
        double       data2 = this.data2;
        String       data3 = new String(this.data3);
        StringBuffer data4 = new StringBuffer(this.data4.toString());
        
        CloneableClass copy = new CloneableClass(data1, data2, data3, data4);
        return copy;
    }// clone
 
}/*CloneableClass*/

客户端代码:

public class CloneableDemo {

    public static void main(String[] args) throws CloneNotSupportedException {
        Test test1 = new Test("original");
        StringBuffer strBuf = new StringBuffer("original");
        
        CloneableClass org = new CloneableClass(test1, 1.0, "original", strBuf);
        CloneableClass copy = null;
        Object objTemp = org.clone();
        if (objTemp instanceof CloneableClass) {
            copy = (CloneableClass)objTemp;
        }// if
        
        System.out.println("Before modify, data of original:");
        org.show();
        System.out.println();
        System.out.println("Before modify, data of copy:");
        copy.show();
        
        // 判断两个对象中的引用型字段是否指向了同一个对象实例。
        System.out.println();
        System.out.println("org.data1 == copy.data1? " + (org.data1 == copy.data1));
        System.out.println("org.data2 == copy.data2? " + (org.data2 == copy.data2));
        System.out.println("org.data3 == copy.data3? " + (org.data3 == copy.data3));
        System.out.println("org.data4 == copy.data4? " + (org.data4 == copy.data4));
        
        // 修改copy中各字段指向对象的属性
        copy.data1.userData = "Copy";
        copy.data2 = 2.0;
        copy.data3 = "Copy";
        copy.data4.replace(0, copy.data4.length(), "Copy");

        System.out.println();
        System.out.println("After modify, data of original:");
        org.show();
        System.out.println();
        System.out.println("After modify, data of copy:");
        copy.show();
    }// main

}/*CloneableDemo*/

运行结果:

JavaSE学习随笔(一) Cloneable接口源码分析与技术细节_第3张图片

       可以看出,从copy和org中相应字段已经指向了不同的实例对象,对copy的修改也不会对org产生影响。






你可能感兴趣的:(JavaSE学习随笔)