前言
在上一篇原型模式博客的基础上,今天第二次写,会详细讲解一下从浅拷贝到深拷贝的实现,我也有专门写过一篇关于浅拷贝与深拷贝的文章,先将这两篇博客链接放在这里
Clone方法
复制值类型变量
在Java中,clone()方法是一个用于对象复制的方法。它定义在java.lang.Object类中,可以被任何类继承和使用。
clone()方法的作用是创建并返回一个当前对象的副本(也称为克隆)。该副本是一个独立的对象,与原始对象具有相同的状态和属性。通常情况下,克隆对象和原始对象是相互独立的,对克隆对象的修改不会影响原始对象。
要使用clone()方法,需要满足以下两个条件:
目标类(要进行克隆的类)必须实现Cloneable接口,否则在调用clone()方法时会抛出CloneNotSupportedException异常。
clone()方法必须在目标类中重写,并且访问修饰符不能是私有的。重写时,可以选择调用super.clone()方法来创建副本,并适当地处理可能存在的引用类型成员变量。
下面看一段代码
class MyClass implements Cloneable { private int value; public MyClass(int value) { this.value = value; } public void setValue(int value) { this.value = value; } public int getValue() { return value; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Main { public static void main(String[] args) { MyClass obj1 = new MyClass(10); try { // 调用clone()方法创建副本 MyClass obj2 = (MyClass) obj1.clone(); System.out.println("obj1 value: " + obj1.getValue()); System.out.println("obj2 value: " + obj2.getValue()); obj2.setValue(20); System.out.println("After modifying obj2:"); System.out.println("obj1 value: " + obj1.getValue()); System.out.println("obj2 value: " + obj2.getValue()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
输出结果为:
obj1 value: 10 obj2 value: 10 After modifying obj2: obj1 value: 10 obj2 value: 20
上面的例子,通过clone()方法创建的obj2对象与原始对象obj1具有相同的初始值。但是,在修改obj2的值后,并不会对obj1产生影响,它们是相互独立的对象。
但是,clone()方法是浅拷贝,只复制对象的值类型成员变量,对于引用类型成员变量,仅复制引用而不复制对象本身。
引用类型成员变量只复制引用
当类的属性是引用类型时,如果不进行深拷贝操作,拷贝对象将与原有对象共享引用。
class MyClass implements Cloneable { private int[] array; public MyClass(int[] array) { this.array = array; } public void setArray(int[] array) { this.array = array; } public int[] getArray() { return array; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } public class Main { public static void main(String[] args) { int[] originalArray = {1, 2, 3}; MyClass obj1 = new MyClass(originalArray); try { MyClass obj2 = (MyClass) obj1.clone(); System.out.println("obj1 array: " + Arrays.toString(obj1.getArray())); System.out.println("obj2 array: " + Arrays.toString(obj2.getArray())); obj2.getArray()[0] = 100; System.out.println("After modifying obj2:"); System.out.println("obj1 array: " + Arrays.toString(obj1.getArray())); System.out.println("obj2 array: " + Arrays.toString(obj2.getArray())); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
输出结果为:
obj1 array: [1, 2, 3] obj2 array: [1, 2, 3] After modifying obj2: obj1 array: [100, 2, 3] obj2 array: [100, 2, 3]
上面的代码可以看到,在将原始对象的数组修改后,拷贝对象的数组也被修改了。这是因为在浅拷贝中,clone()方法只复制了引用,而没有复制数组本身。因此,原始对象和拷贝对象共享同一个数组实例。
浅拷贝变深拷贝
如果想要实现深拷贝,需要在clone()方法中对数组进行复制操作,以避免共享引用。
class MyClass implements Cloneable { private int[] array; public MyClass(int[] array) { this.array = array; } public void setArray(int[] array) { this.array = array; } public int[] getArray() { return array; } @Override public Object clone() throws CloneNotSupportedException { MyClass cloned = (MyClass) super.clone(); cloned.array = array.clone(); // 深拷贝数组 return cloned; } } public class Main { public static void main(String[] args) { int[] originalArray = {1, 2, 3}; MyClass obj1 = new MyClass(originalArray); try { MyClass obj2 = (MyClass) obj1.clone(); System.out.println("obj1 array: " + Arrays.toString(obj1.getArray())); System.out.println("obj2 array: " + Arrays.toString(obj2.getArray())); obj2.getArray()[0] = 100; System.out.println("After modifying obj2:"); System.out.println("obj1 array: " + Arrays.toString(obj1.getArray())); System.out.println("obj2 array: " + Arrays.toString(obj2.getArray())); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
输出结果为:
obj1 array: [1, 2, 3] obj2 array: [1, 2, 3] After modifying obj2: obj1 array: [1, 2, 3] obj2 array: [100, 2, 3]
修改拷贝对象的数组后,原始对象的数组并没有受到影响。这是因为在深拷贝中,对数组进行了复制操作,使得原始对象和拷贝对象拥有独立的数组实例。
如果需要实现深拷贝,也就是复制对象及其引用类型成员变量所指向的对象,就需要在clone()方法中进行相应处理。
示例详解
如果上面的代码没有看懂,就跟着我再看一个示例,这个例子在上篇原型模式的博客中有详细讲过衍化过程,在这我只说最后一版,先看代码
//工作经验类也实现cloneable接口 public class WorkExperience implements Cloneable{ private String timeArea; public String getTimeArea() { return timeArea; } public void setTimeArea(String timeArea) { this.timeArea = timeArea; } //所在公司 private String company; public String getCompany() { return company; } public void setCompany(String company) { this.company = company; } public WorkExperience clone(){ WorkExperience object=null; try { object=(WorkExperience) super.clone(); } catch (CloneNotSupportedException e) { System.err.println("clone异常"); } return object; } } //简历类 public class Resume implements Cloneable{ private String name; private String sex; private String age; private WorkExperience work;//声明一个工作经历对象 public Resume(String name){ this.name=name; this.work=new WorkExperience();//实例化一个工作 经历对象 } public void setPersonalInfo(String sex,String age){ this.age=age; this.sex=sex; } //设置工作经历 public void setWorkExperience(String timeArea,String company){ this.work.setTimeArea(timeArea); this.work.setCompany(company);; } //展示简历 public void display(){ System.out.println(this.name+" " +this.sex+" "+this.age); System.out.println("工作经历"+this.work.getTimeArea()+" " +this.work.getCompany()); } public Resume clone(){ Resume object=null; try { object=(Resume) super.clone(); this.work= this.work.clone();//对克隆对象里的引用也进行克隆,就达到了深复制的作用 } catch (CloneNotSupportedException e) { System.err.println("clone异常"); } return object; } //客户端 public class Client { public static void main(String[] args) { Resume resume1=new Resume("张三"); resume1.setPersonalInfo("男","30"); resume1.setWorkExperience("2000-2010","Xx公司"); Resume resume2=resume1.clone(); resume2.setWorkExperience("2001-2011","YY集团"); Resume resume3=resume1.clone(); resume3.setPersonalInfo("女","35"); resume3.setWorkExperience("2020-2023","ZZ公司"); resume1.display(); resume2.display(); resume3.display(); } }
代码中主要有三个类
简历类、工作经验类和客户端
其中简历类中有四个属性,三个数值型和一个引用型属性-WorkExperience
根据上面的介绍,如果Resume直接实现clone方法,那么只有三个数值型的属性可以直接复制出来,但是引用型的WorkExperience 只能复制引用,如下图所示,Resume是原型,Resume 2 是拷贝出来的对象,work就是WorkExperience类型的变量,所谓的复制引用,就是只把内存地址复制了一下,也就是Resume和Resume 2中的work都指向了同一块内存,那么可以想象一些,如果Resume 2修改了00110地址中的内容,原型Resume中的work的值也会发生变化,因为Resume和Resume 2 共享同一个WorkExperience对象。
如果不想Resume 2改动会影响Resume ,那就需要把work所指向的对象复制一份,work里timeArea和company,都是String类型。
虽然String在Java中是引用类型,但是字符串是不可变的,即创建后不能被修改。这意味着对字符串进行修改实际上是创建了一个新的字符串对象。在浅拷贝中,如果原始对象包含String类型的属性,那么在进行浅拷贝时,拷贝出来的对象会与原始对象共享同一个字符串对象的引用。由于字符串是不可变的,因此共享引用并不会导致问题,因为无法修改字符串的内容。也就是浅拷贝对于String类型的属性是有效的,所以WorkExperience也实现了Cloneable接口中的clone方法。
接下来,就是Resume 怎么从浅拷贝到深拷贝。看下面的图
图中圈住的1和2就是实现的原理–对克隆对象里的引用也进行克隆,就达到了深复制的作用
直接让WorkExperience类型的对象work进行浅复制(前面讲到的work里的string类型变量,浅拷贝对于String类型的属性有效),在Resume里复制一份新的work对象。
如果原型对象的引用类型属性中还包含了引用类型属性,那么需要进行递归地深拷贝来确保所有层级的引用类型属性都被复制
注意事项
在原型模式中进行深拷贝时,需要注意以下几点:
- 引用类型属性的处理:如果原型对象中包含引用类型的属性,那么在进行深拷贝时,需要确保引用类型属性也被正确复制而不是简单的浅拷贝。否则,在拷贝对象中修改引用类型属性可能会影响到原始对象。
- 性能和效率:深拷贝可能会消耗更多的系统资源和时间,特别是在对象结构比较复杂、对象数量较多的情况下。因此,在实现深拷贝时需要考虑性能和效率,避免不必要的资源浪费。
- 对象图的复制:在进行深拷贝时,需要确保整个对象图都被正确地复制,即使对象之间存在相互引用或循环引用的情况,也需要能够正确地处理和复制。
- 序列化与反序列化:使用序列化和反序列化是一种常见的实现深拷贝的方法,但需要确保对象及其所有引用类型属性都能够被序列化和反序列化,以及处理可能出现的异常情况。
- 兼容性和可维护性:在实现深拷贝时需要考虑代码的兼容性和可维护性,确保代码易于理解和扩展,并且不会引入潜在的bug或问题。
总结
原型模式是一种创建型设计模式,它允许通过复制现有对象来创建新对象,而不是通过实例化类来创建。在原型模式中,深拷贝是指创建一个新对象,并且递归地复制对象及其所有引用类型属性,以确保新对象与原始对象完全独立。