Clonable接口以及再次理解深拷贝与浅拷贝!!(面试常考)

简介: Clonable接口以及再次理解深拷贝与浅拷贝!!(面试常考)

Clonable接口

在抽象类和接口的博客中,我们讲到了两个我们会常用到的接口,一个是Compareable接口,一个是Comperator接口,现在我们再来讲一个我们会经常用到的接口,也就是我们的Clonable接口.


首先我们来思考一个问题:如何进行对象的拷贝?

Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝". 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常.

我们先来看Clonable接口的源码:

2.png


我们可以看到这类接口什么都没有,那么我们称这类接口为空接口,也就是标记接口.

空接口的意义:标记当前这个类是可以被克隆的.克隆会做一件事情,拷贝一份副本.


同时关于对象的拷贝会牵扯出之前我们所提到的深拷贝和浅拷贝的问题,下面我们来进行对比:


Clonable接口结合深拷贝问题与浅拷贝问题

首先一个对象的引用如果想要进行克隆,那么其对应的类必须实现Clonable 接口,并且重写Object类中的clone方法,并且重写的clone方法后必须抛出CloneNotSupportedException 异常.


Clonable接口结合深拷贝

我们直接通过代码来进行解析:先来看一段代码:


1.class Person implements Cloneable {  
2.    public int age;  
3.    @Override  
4.   //此处的clone方法必须抛出CloneNotSupportedException异常  
5.    protected Object clone() throws CloneNotSupportedException {  
6.        return super.clone();  
7.    }  
8.}  
9.//此处的主函数也需要抛出这个异常
10. public static void main(String[] args) throws CloneNotSupportedException {  
11.        Person person = new Person();  
12.        /*注意对于下面的调用clone方法有两点需要注意: 
13.        1:首先引用在调用clone方法时main方法需要抛出CloneNotSupportedException异常 
14.        2:因为clone方法的返回值为引用类型Object,所以如果是其他类定义的引用就需要强制类型转换 
15.         */  
16.        Person person1 = (Person) person.clone();  
17.        System.out.println(person.age);  
18.        System.out.println(person1.age);  
19.        System.out.println("===========对比===========");  
20.        person1.age = 10;  
21.        System.out.println(person.age);  
22.        System.out.println(person1.age);  
23.    }  

此段代码的输出结果为:

2.png

此时我们会发现首先我们对person所指向的对象进行了拷贝,并把它赋给了一个新的引用person1,那么此时在内存是什么样的呢?我们来看下图所示:

2.png

此时我们可以看到通过clone方法我们在堆上克隆了一个副本,并将这个副本对象赋给了一个新的引用,并且当我们修改person1这个引用所指向对象中所包含的简单类型age的值时,person这个引用所指向对象中所包含i的简单类型age的值并没有发生改变,那么这种情况我们便称之为深拷贝,即当拷贝结束后,通过一个新的引用修改所拷贝的新的对象的其中的某个类型的值时,并不影响原来引用所对应的相同对象中的相同类型的值,那么此时便为深拷贝


Clonable接口结合浅拷贝

下面我们来介绍浅拷贝:

因为之前堆上的对象内部存储都是简单类型,所以都是深拷贝,那么如果存储引用类型后,就会发生浅拷贝了,下面来看代码:

1.class Money {  
2.    double money = 12.6;  
3.}  
4.  
5.class Person implements Cloneable {  
6.    public int age;  
7.    Money m = new Money();  
8.  
9.    @Override  
10.    //此处的clone方法必须抛出CloneNotSupportedException异常  
11.    protected Object clone() throws CloneNotSupportedException {  
12.        return super.clone();  
13.    }  
14.}  
15.  
16.public class TestDemo {  
17.    public static void main(String[] args) throws CloneNotSupportedException {  
18.        Person person = new Person();  
19.        Person person1 = (Person) person.clone();  
20.        System.out.println(person.m.money);  
21.        System.out.println(person1.m.money);  
22.        System.out.println("===========对比===========");  
23.        person1.m.money = 99.9;  
24.        System.out.println(“修改后为”+person.m.money);  
25.        System.out.println(“修改后为”+person1.m.money);  
26.    }  
27.}  

此时我们新建了一个Money类,并在Person类中加入了Money类的实例,那么此时Person类的实例在堆上的存储中便会多出一个引用类型,那么来看下其在内存上的存储示意图吧:

2.png

此时我们会发现我们通过clone方法仅仅将简单类型age和引用类型m克隆了过来,但是m所指向的对象我们并没有克隆过来,那么两者的m引用都将会指向同一个对象

此时如果我们通过person1.m.money去修改money的值为99.9时,我们会发现原引用person所对应的money值也随之变成了99.9,那么这种现象我们称之为浅拷贝,即其当一个新的引用去修改其克隆过来的对象中的某个类型(此段代码为引用类型)的值后,原引用调用这个类型时便会发现这个值为新修改后的值了,并不是原来的值。

所以上述代码中的最终结果我们会发现当其中一个引用修改了值后,另一个引用去调用时也会得到相同的值,我们来看运行结果以证实我们的猜想吧

2.png

果然就算修改过后两者的值依然相等


将深拷贝改为浅拷贝的方法

此时就会有同学有疑问了,上述代码如何可以实现深拷贝呢?

答案非常简单:此时我们让Money类实现Cloneable接口就好。目的就是为了将Money类的实例也能通过clone方法拷贝过来,下面来看代码:

1.class Money  implements Cloneable {  
2.    double money = 12.6;  
3.  
4.    @Override  
5.    protected Object clone() throws CloneNotSupportedException {  
6.        return super.clone();  
7.    }  
8.}  
9.  
10.class Person implements Cloneable {  
11.    public int age;  
12.     Money m = new Money();  
13.  
14.    //重写Object类中的clone方法 ,并且必须抛出CloneNotSupportedException异常
15.    @Override  
16.    protected Object clone() throws CloneNotSupportedException {  
17.        //克隆person所指向的对象  
18.        Person p = (Person) super.clone();  
19.        //克隆Person类中的另一个引用类型m所指向的对象  
20.        p.m = (Money) this.m.clone();  
21.        return p;  
22.    }  
23.}  
24.  
25.public class TestDemo {  
26.    public static void main(String[] args) throws CloneNotSupportedException {  
27.        Person person = new Person();  
28.        Person person1 = (Person) person.clone();  
29.        System.out.println(person.m.money);  
30.        System.out.println(person1.m.money);  
31.        System.out.println("===========对比===========");  
32.        person1.m.money = 99.9;  
33.        System.out.println("修改后的结果为"+person.m.money);  
34.        System.out.println("修改后的结果为"+person1.m.money);  
35.    }  
36.}  

内存示意图:

2.png

最终的输出结果为:

2.png

此时我们发现当通过语句person1.m.money修改money值后,并不影响person引用所指向对象中的m引用所指向对象中的money值,那么此时便发生了深拷贝,我们的目的达到了。


相关文章
|
4月前
|
Java
【Java基础面试三十四】、接口中可以有构造函数吗?
这篇文章讨论了Java中接口不能包含构造函数的原因,主要解释了接口中的成员变量默认是public static final类型的常量,不需要通过构造函数初始化,且接口本身不能被实例化,因此构造函数在接口中没有意义。
|
4月前
|
Java
【Java基础面试三十八】、请介绍Java的异常接口
这篇文章介绍了Java的异常体系结构,主要讲述了Throwable作为异常的顶层父类,以及其子类Error和Exception的区别和处理方式。
【IO面试题 五】、 Serializable接口为什么需要定义serialVersionUID变量?
serialVersionUID用于标识类的序列化版本,确保在反序列化时类的版本一致性,避免因类定义变更导致的不兼容问题。
|
4月前
|
Java
【Java基础面试三十五】、谈谈你对面向接口编程的理解
这篇文章讨论了面向接口编程的概念,强调接口作为一种规范和实现分离的设计哲学,可以降低程序模块间的耦合度,提高系统的可扩展性和可维护性。
|
4月前
|
SQL 安全 测试技术
[go 面试] 接口测试的方法与技巧
[go 面试] 接口测试的方法与技巧
|
4月前
|
安全 Java
【Java集合类面试三】、Map接口有哪些实现类?
这篇文章介绍了Java中Map接口的几种常用实现类:HashMap、LinkedHashMap、TreeMap和ConcurrentHashMap,以及它们适用的不同场景和线程安全性。
|
4月前
|
自然语言处理 NoSQL Java
一天一道Java面试题----第十二天(如何实现接口幂等性)
这篇文章探讨了实现Java接口幂等性的几种方法,包括使用唯一ID、服务端token、去重表、版本控制以及控制状态等策略。
|
4月前
|
存储 JavaScript 前端开发
JS浅拷贝及面试时手写源码
JS浅拷贝及面试时手写源码
|
5月前
|
Java API
Java面试题:说明Lambda表达式在Java中的应用,以及函数式接口的概念和作用。
Java面试题:说明Lambda表达式在Java中的应用,以及函数式接口的概念和作用。
38 0
|
5月前
|
网络协议 Java
Java面试题:什么是Java中的接口?与抽象类有什么区别?
Java面试题:什么是Java中的接口?与抽象类有什么区别?
41 0