对象克隆学习

假如说你想复制一个简单变量。很简单:

int apples = 5;  
int pears = apples;  

不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。

但是如果你复制的是一个对象,情况就有些复杂了。

假设说我是一个beginner,我会这样写:

复制代码

class Student {  private int number;  public int getNumber() {  return number;  }  public void setNumber(int number) {  this.number = number;  }  }  
public class Test {  public static void main(String args[]) {  Student stu1 = new Student();  stu1.setNumber(12345);  Student stu2 = stu1;  System.out.println("学生1:" + stu1.getNumber());  System.out.println("学生2:" + stu2.getNumber());  }  
}  

复制代码

结果:

学生1:12345  

学生2:12345  

这里我们自定义了一个学生类,该类只有一个number字段。

我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)

再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

难道真的是这样吗?

我们试着改变stu2实例的number字段,再打印结果看看:

stu2.setNumber(54321);  System.out.println("学生1:" + stu1.getNumber());  
System.out.println("学生2:" + stu2.getNumber());  

结果:

学生1:54321  

学生2:54321  

这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢?

原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,

这样,stu1和stu2指向内存堆中同一个对象。如图:

那么,怎样才能达到复制一个对象呢?

是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。

在Java中所有的类都是缺省的继承自Java语言包中的Object类的,查看它的源码,你可以把你的JDK目录下的src.zip复制到其他地方然后解压,里面就是所有的源码。发现里面有一个访问限定符为protected的方法clone():

复制代码

/*
Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
The general intent is that, for any object x, the expression:
1) x.clone() != x will be true
2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
3) x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object clone() throws CloneNotSupportedException;

复制代码

仔细一看,它还是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

  1. 第一次声明保证克隆对象将有单独的内存地址分配。
  2. 第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。
  3. 第三声明表明,原始和克隆的对象应该是平等的equals()方法使用,但它不是强制性的。

因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。

要想对一个对象进行复制,就需要对clone方法覆盖。

回到顶部

为什么要克隆?

  大家先思考一个问题,为什么需要克隆对象?直接new一个对象不行吗?

  答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。

  提个醒,我们常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。

  而通过clone方法赋值的对象跟原来的对象时同时独立存在的。

回到顶部

如何实现克隆

先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)深克隆(DeepClone)

在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

一般步骤是(浅克隆):

1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)

2. 覆盖clone()方法,访问修饰符设为public方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)

下面对上面那个方法进行改造:

复制代码

class Student implements Cloneable{  private int number;  public int getNumber() {  return number;  }  public void setNumber(int number) {  this.number = number;  }  @Override  public Object clone() {  Student stu = null;  try{  stu = (Student)super.clone();  }catch(CloneNotSupportedException e) {  e.printStackTrace();  }  return stu;  }  
}  
public class Test {  public static void main(String args[]) {  Student stu1 = new Student();  stu1.setNumber(12345);  Student stu2 = (Student)stu1.clone();  System.out.println("学生1:" + stu1.getNumber());  System.out.println("学生2:" + stu2.getNumber());  stu2.setNumber(54321);  System.out.println("学生1:" + stu1.getNumber());  System.out.println("学生2:" + stu2.getNumber());  }  
}  

复制代码

结果:

学生1:12345  

学生2:12345  

学生1:12345  

学生2:54321

如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

System.out.println(stu1 == stu2); // false  

上面的复制被称为浅克隆。

还有一种稍微复杂的深度复制:

我们在学生类里再加一个Address类。

复制代码

 1 class Address  {  2     private String add;  3   4     public String getAdd() {  5         return add;  6     }  7   8     public void setAdd(String add) {  9         this.add = add;  
10     }  
11       
12 }  
13   
14 class Student implements Cloneable{  
15     private int number;  
16   
17     private Address addr;  
18       
19     public Address getAddr() {  
20         return addr;  
21     }  
22   
23     public void setAddr(Address addr) {  
24         this.addr = addr;  
25     }  
26   
27     public int getNumber() {  
28         return number;  
29     }  
30   
31     public void setNumber(int number) {  
32         this.number = number;  
33     }  
34       
35     @Override  
36     public Object clone() {  
37         Student stu = null;  
38         try{  
39             stu = (Student)super.clone();  
40         }catch(CloneNotSupportedException e) {  
41             e.printStackTrace();  
42         }  
43         return stu;  
44     }  
45 }  
46 public class Test {  
47       
48     public static void main(String args[]) {  
49           
50         Address addr = new Address();  
51         addr.setAdd("杭州市");  
52         Student stu1 = new Student();  
53         stu1.setNumber(123);  
54         stu1.setAddr(addr);  
55           
56         Student stu2 = (Student)stu1.clone();  
57           
58         System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
59         System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
60     }  
61 }  

复制代码

结果:

学生1:123,地址:杭州市  

学生2:123,地址:杭州市  

乍一看没什么问题,真的是这样吗?

我们在main方法中试着改变addr实例的地址。

addr.setAdd("西湖区");  System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  

结果:

学生1:123,地址:杭州市  
学生2:123,地址:杭州市  
学生1:123,地址:西湖区  
学生2:123,地址:西湖区  

这就奇怪了,怎么两个学生的地址都改变了?

原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:

复制代码

 1 package abc;  2   3 class Address implements Cloneable {  4     private String add;  5   6     public String getAdd() {  7         return add;  8     }  9   
10     public void setAdd(String add) {  
11         this.add = add;  
12     }  
13       
14     @Override  
15     public Object clone() {  
16         Address addr = null;  
17         try{  
18             addr = (Address)super.clone();  
19         }catch(CloneNotSupportedException e) {  
20             e.printStackTrace();  
21         }  
22         return addr;  
23     }  
24 }  
25   
26 class Student implements Cloneable{  
27     private int number;  
28   
29     private Address addr;  
30       
31     public Address getAddr() {  
32         return addr;  
33     }  
34   
35     public void setAddr(Address addr) {  
36         this.addr = addr;  
37     }  
38   
39     public int getNumber() {  
40         return number;  
41     }  
42   
43     public void setNumber(int number) {  
44         this.number = number;  
45     }  
46       
47     @Override  
48     public Object clone() {  
49         Student stu = null;  
50         try{  
51             stu = (Student)super.clone();   //浅复制  
52         }catch(CloneNotSupportedException e) {  
53             e.printStackTrace();  
54         }  
55         stu.addr = (Address)addr.clone();   //深度复制  
56         return stu;  
57     }  
58 }  
59 public class Test {  
60       
61     public static void main(String args[]) {  
62           
63         Address addr = new Address();  
64         addr.setAdd("杭州市");  
65         Student stu1 = new Student();  
66         stu1.setNumber(123);  
67         stu1.setAddr(addr);  
68           
69         Student stu2 = (Student)stu1.clone();  
70           
71         System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
72         System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
73           
74         addr.setAdd("西湖区");  
75           
76         System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
77         System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
78     }  
79 }  

复制代码

结果:

学生1:123,地址:杭州市  
学生2:123,地址:杭州市  
学生1:123,地址:西湖区  
学生2:123,地址:杭州市  

这样结果就符合我们的想法了。

最后我们可以看看API里其中一个实现了clone方法的类:

java.util.Date:

复制代码

/** * Return a copy of this object. */  
public Object clone() {  Date d = null;  try {  d = (Date)super.clone();  if (cdate != null) {  d.cdate = (BaseCalendar.Date) cdate.clone();  }  } catch (CloneNotSupportedException e) {} // Won't happen  return d;  
}  

复制代码

该类其实也属于深度复制。

参考文档:Java如何复制对象

回到顶部

浅克隆和深克隆

1、浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆

2、深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。

如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

扩展
Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

回到顶部

解决多层克隆问题

如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

复制代码

 1 public class Outer implements Serializable{2   private static final long serialVersionUID = 369285298572941L;  //最好是显式声明ID3   public Inner inner;4  //Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化] 5   public Outer myclone() {6       Outer outer = null;7       try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝8           ByteArrayOutputStream baos = new ByteArrayOutputStream();9           ObjectOutputStream oos = new ObjectOutputStream(baos);
10           oos.writeObject(this);
11       // 将流序列化成对象
12           ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
13           ObjectInputStream ois = new ObjectInputStream(bais);
14           outer = (Outer) ois.readObject();
15       } catch (IOException e) {
16           e.printStackTrace();
17       } catch (ClassNotFoundException e) {
18           e.printStackTrace();
19       }
20       return outer;
21   }
22 }

复制代码

Inner也必须实现Serializable,否则无法序列化:

复制代码

 1 public class Inner implements Serializable{2   private static final long serialVersionUID = 872390113109L; //最好是显式声明ID3   public String name = "";4 5   public Inner(String name) {6       this.name = name;7   }8 9   @Override
10   public String toString() {
11       return "Inner的name值为:" + name;
12   }
13 }

复制代码

这样也能使两个对象在内存空间内完全独立存在,互不影响对方的值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/329557.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【linux应用开发】进程通信总结——使用管道、消息队列、共享内存、信号量实现l进程通信的详细教程

文章目录 简介无名管道有名管道IPC key标识消息队列共享内存信号量 简介 进程间通信(IPC, Inter-Process Communication)是指在操作系统中,不同进程之间交换数据、信息和命令的过程。在一个多任务的操作系统中,多个进程可以同时运…

【IDEA】 解决在idea中连接 Mysql8.0,驱动无法下载问题

本篇继【idea】解决sprintboot项目创建遇到的问题2-CSDN博客 目录 一、Failed to download https://download.jetbrains.com/idea/jdbc-drivers/MySQL/8/LICENSE.txt:Remote host terminated the handshake 二、no dirver files provided com.mysql.cj.jdbc.Driver 三、Serv…

申请域名SSL证书并自动推送至阿里云 CDN

近期国外SSL证书厂商调整了免费证书的续签规则,一年期的证书全部取消,现在只能申请90天有效期的免费证书。普通web站点可以通过宝塔面板或部署acme.sh等证书自动管理工具来实现自动化申请和部署,但是阿里云之类的CDN服务就只能通过手动或Open…

mac版viso软件 流程图软件omnigraffile

OmniGraffle 是一款由 The Omni Group 开发的绘图工具,主要用于创建各种类型的图表、流程图、组织结构图、网站地图等。它提供了丰富的绘图工具和功能,包括形状、线条、文本、颜色、样式等,可以帮助用户轻松地创建出精美的图表和图形。 OmniG…

通俗易懂的15个Java Lambda表达式案例

文章目录 1. **实现Runnable接口**:2. **事件监听器**(如Swing中的ActionListener):3. **集合遍历**(使用forEach方法):4. **过滤集合**(使用Stream API):5. …

Python库学习(十四):ORM框架-SQLAlchemy

1.介绍 SQLAlchemy 是一个用于 Python 的 SQL 工具和对象关系映射(ORM)库。它允许开发者通过 Python 代码而不是 SQL查询语言来操作数据库。SQLAlchemy 提供了一种灵活且强大的方式来与关系型数据库交互,支持多种数据库后端,如 P…

学习笔记:C++之 switch语句

Switch语句 作用:执行多条件分支语句 语法: switch(表达式){ case 结果1:执行语句;break; case 结果2:执行语句;break; ... default:执行语句&a…

【51单片机】独立按键控制LED灯

不同于上篇文章只用代码控制,这次我们要再加上独立按键一同控制LED灯 目录 独立按键控制LED亮灭:代码实现: 独立按键控制LED状态:代码实现: 独立按键实现二进制LED显示:代码实现: 独立按键控制…

1_工程设计模式-简单工厂设计

工程设计模式 一.简单工程设计模式 1.概念 简单工厂模式( Simple Factory Pattern )是指由一个工厂对象决定创建出哪一种产品类的实例。 但它不属于 GOF 23 种设计模式。简单工厂适用于工厂类负责创建的对象较少的场景,且客户端只需要传入工厂类的参数&#xff…

Qt/QML编程学习之心得:Linux下USB接口使用(25)

很多linux嵌入式系统都有USB接口,那么如何使用USB接口呢? 首先,linux的底层驱动要支持,在linux kernal目录下可以找到对应的dts文件,(device tree) usb0: usb@ee520000{compatible = "myusb,musb";status = "disabled";reg = <0xEE520000 0x100…

【两阶段鲁棒】计及需求响应的多能互补微网两阶段鲁棒优化matlab

目录 1 主要内容 算例模型 目标函数 第一阶段 第二阶段 求解流程图 2 部分程序 3 程序结果 4 下载链接 1 主要内容 该程序参考文献《多能互补微网两阶段鲁棒优化调度研究》&#xff0c;在考虑风光不确定集的基础上提出采用计及DR响应的多能互补微网两阶段鲁棒备用调度模…

thinkphp6实现简单定时任务

thinkphp6实现定时任务 创建定时任务文件定义指令编写Test.php代码运行测试 创建定时任务文件 Test类名根据自己的需要修改 php think make:command Test testcommand文件夹在app目录下没有需要自己创建 运行上面的命令后会在command下 多一个Test.php文件 定义指令 在conf…