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

JavaSE学习随笔(一) Cloneable接口源码分析与技术细节Cloneable接口是Java开发中常用的一个接口,它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中,注意,这里所说的“拷贝”拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。本博文将从Cloneable接口的源码入手,对其技术细节和使用方法进行详细的介绍。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

一、引言

 

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

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


二、源码分析


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

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

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

<span style="font-size: 18px;">/*Object类中clone()方法的定义*/
protected native Object clone() throws CloneNotSupportedException;</span>

      看到这里,有同学可能会想,既然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接口源码分析与技术细节

       从执行结果上来看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接口源码分析与技术细节

       从运行结果可以看出,由于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接口源码分析与技术细节

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






版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/183454.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • CreatePipe匿名管道通信

    CreatePipe匿名管道通信管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。匿名管道(AnonymousPipes)是在父进程和子进程间单向传输数据的一种未命名的管道,只能在本地计算机中使用,而不可用于网络间的通信。      匿名管道实施细则      匿名管道由Cre

  • idea右键没有run选项,无法运行main方法及启动springboot工程

    idea右键没有run选项,无法运行main方法及启动springboot工程idea右键没有run选项,无法运行main方法及启动springboot工程在idea中有的时候会从外部导入项目,导入成功后打开某个类,需要运行里面的main方法.按如常操作单击右键发现没有run选项。解决办法:选中你java文件所在的package单击右键选中MarkDirectoryas,然后选中SourcesRoot。例如我下图中的Thread1.java中的mai’n方法无法运…

  • 出现namenode不能启动的情况,就把hadoop安装目录下的hadoop目录下的data和name文件夹清空,[通俗易懂]

    出现namenode不能启动的情况,就把hadoop安装目录下的hadoop目录下的data和name文件夹清空,[通俗易懂]出现namenode不能启动的情况,就把hadoop安装目录下的hadoop目录下的data和name文件夹清空,

  • Eclipse 添加 Tomcat Server 配置

    Eclipse 添加 Tomcat Server 配置以下步骤是将一个独立安装的(standalone)Tomcat整合到Eclipse中,方便在Eclipse发布Web工程到Tomcat服务器,启停WebServer调试程序。项目开发中不推荐使用Eclipse自带的WebServer,不便于运行调试,往往需要根据项目需求独立安装指定厂家和版本的Webserver。(项目部署参考–>Eclipse部署项目到Tomcat)…

    2022年10月25日
  • plugins webpack_webpack plugin原理

    plugins webpack_webpack plugin原理plugin插件是webpack的支柱功能。webpack自身也是构建于你在webpack配置中用到的相同的插件系统之上!插件目的在于解决loader无法实现的其他事。常用的插件

  • laravel 自定义常量方法

    laravel 自定义常量方法

    2021年10月24日

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号