原型模式及深浅拷贝
通过 new
关键字生成实例是需要指定类名的。在开发过程中,我们需要「在不指定类名的前提下生成实例」的需求。
在以下情况,我们就不能根据类来生成实例,而要根据现有的实例来生成新的实例。
- 对象种类繁多,无法将它们整合到一个类中。需要处理的对象太多,如果将它们分别作为一个类,必须要编写很多个类文件。
- 难以根据类生成实例。生成实例的过程太复杂。
- 想解耦框架与生成的实例时。想让生成实例的框架不依赖于具体的类。
- 从外部复制对象并非总是可行,因为外部可能访问不到内部的私有变量。
原型模式:
- 在软件系统中,有时候需要多次创建某一类型的对象,为了简化创建过程,可以只创建一个对象,然后再通过克隆的方式复制出多个相同的对象。原型模式将克隆过程委派给被克隆的实际对象。 支持克隆的对象即为原型。
- 原型模式不跟据类生成实例,而是根据实例来生成新实例。在 Java 中,可以使用
clone
创建出实例的副本。 - 原型模式在创建重复对象的同时,需要保证性能。可以通过
clone()
拷贝已有对象的数据,更新少量差值来实现。 - 克隆方式:创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。甚至可以复制私有成员变量,因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。
原型模式的基本工作原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象复制原型自己来实现创建过程。
登场角色:
Prototype
(抽象原型类):定义用于复制现有实例来生成新实例的方法,可以是抽象类也可以是接口。在 Java 中,这个抽象类是实现了@Cloneable
接口的。ConcretePrototype
(具体原型类):实现复制现有实例并生成新实例的方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等等。Client
(使用者):使用复制实例的方法生成新实例。
优点 | 缺点 |
---|---|
解耦。克隆对象释放无需与它们所属的类相耦合。 | 克隆包含循环引用的复杂对象可能会非常麻烦。 |
克隆预生成原型,避免反复运行初始化代码。 | 需要为每一个类配备一个克隆方法,因此对已有类进行改造比较麻烦。需要修改其源代码,并且在实现深克隆时需要编写较为复杂的代码。 |
方便生成复杂对象。 | |
用继承以外的方式来处理复杂对象的不同配置。 |
相关的设计模式:
- 站内文章享元模式:使用原型模式可以生成一个与当前实例的状态完全相同的实例。而使用享元模式可以在不同的地方使用同一个实例。
- 站内文章备忘录模式:使用原型模式可以生成一个与当前实例的状态完全相同的实例。而使用备忘录模式可以保存当前实例的状态,以实现快照和撤销功能。有时候,原型可以作为备忘录模式的一个简化版本,其条件是你需要在历史记录中存储的对象的状态比较简单,不需要链接其他外部资源,或者链接可以方便地重建。
- 站内文章组合模式 以及 站内文章装饰器模式:经常使用组合模式和装饰器模式时,需要能够动态地创建复杂结构的实例。这时可以使用原型模式,以帮助我们方便地生成实例。
- 站内文章命令模式:原型模式可以复制命令模式中出现的命令,保存为历史记录。
- 站内文章工厂方法模式:原型并不基于继承,因此没有继承的缺点。另一方面,原型需要对被复制对象进行复杂的初始化。工厂方法基于继承, 但是它不需要初始化步骤。
示例:带原型管理器的原型模式
《图解设计模式》第六章中的案例其实就是使用了原型管理器。
原型管理器(Prototype Manager)也称为原型注册表。这个角色创建具体原型类的对象,并记录每一个被创建的对象。原型管理器的作用与工厂相似,其中定义了一个集合用于存储原型对象,如果需要某个对象的一个克隆,可以通过复制集合中对应的原型对象来获得。在原型管理器中针对抽象原型类进行编程,以便扩展。
案例 1:Color
抽象原型类 MyColor
:
1 | public interface MyColor extends Cloneable { |
具体原型类 Red
:
1 | public class Red implements MyColor { |
具体原型类 Blue
:
1 | public class Blue implements MyColor { |
原型管理器类 PrototypeManager
:
1 | public class PrototypeManager { |
客户端测试类 Client
:
1 | public class Client { |
其他案例
光看 UML 就能知道大概实现了。
引用拷贝和对象拷贝
对象和引用的区别:
- 对象:绝大多数对象在堆区,它是实际保存属性的内存空间
- 引用:引用大多引用在栈区,可以将它理解为指向实际对象地址的指针
一个对象可以有多个引用,但一个引用只能指向一个对象。当我们使用 ==
比较对象时,一般比较的是对象地址。
引用拷贝
引用拷贝示例:
1 | Teacher teacher = new Teacher("Taylor",26); |
引用拷贝通常发生在传递参数、返回值等场景中。例如,在 Java 中,如果将一个对象作为参数传递给方法,实际上是将该对象的引用传递给了方法,而不是对象本身的拷贝。
对象拷贝
对象拷贝:创建对象本身的一个副本。
1 | Teacher teacher = new Teacher("Swift",26); |
深拷贝与浅拷贝
深拷贝和浅拷贝都是对象拷贝。它们之间的主要区别在于是否复制了对象内部的数据。
- 浅拷贝 Shallow Copy:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
- 深拷贝 Deep Copy:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象,换言之,深复制把要复制的对象所引用的对象都复制了一遍。
🍐⚱️:(谈浅拷贝)你用魔法棒复制了自己,但是复制人和你都用着同样一台电脑)
在上图的例子中,单纯使用
clone()
方法得到的是浅克隆的结果。
Java Object
类中的 clone()
方法
clone
用于复制实例。clone
方法定义在 java.lang.Object
中,Object
又是所有类的父类,也就是说所有的 Java 类都继承了 clone
方法。但是,继承了该方法并不意味着就能直接用。能调用 clone
方法的前提是这个类必须实现了 Cloneable
接口。
Object
类中的 clone()
方法是通过 native
修饰的,也就是说它是通过直接调用底层操作系统方法实现的。当没有重写该方法时就默认调用这个方法。
clone()
和 new
一样,都可以创建一个新的对象。
new
:当我们使用new
关键字创建对象时,JVM 首先根据关键字后面的类型确定需要申请的内存大小,申请完内存后,执行类的构造方法。在执行构造方法期间,填充内存中各个属性域,这个填充的过程也叫初始化。构造方法执行完标志着对象创建成功,此时返回对象地址,在栈区以引用的方式调用对象。(当然,由于指令的重排序,发布对象可能在构造函数返回之前)clone()
:当我们调用某个对象的clone()
方法克隆对象时,首先根据原对象的内存大小申请内存空间,申请完内存空间后,将原对象内存域复制到新申请的内存空间,复制完成标志着克隆完成,返回引用类型。也就是说,clone()
的过程没有调用类的构造函数。
简单总结:new
和 clone()
第一步都是申请内存,只不过 new
关键字通过类构造方法初始化对象,clone()
方法直接通过克隆内存域完成对象创建。
clone()
是深克隆还是浅克隆?
- 如果一个对象中所有属性都是基础类型(
int
,boolean
),那么它的深克隆和单纯clone()
的浅克隆结果完全相同。 - 如果一个对象包含引用类型数据,如果克隆之后的引用所指向对象是不同对象(看你
clone()
的具体实现),那么它是深克隆,否则是浅克隆。
深拷贝的实现
手动赋值、第三方库也可以实现深拷贝。
Cloneable
:重写 clone
方法
在编码过程中,一般都是通过实现 java.lang.Cloneable
接口重写 clone()
方法,然后就可以调用重写后的 clone()
方法执行自己的逻辑。Cloneable
接口中并没有声明任何方法。他只是用来标记哪个类可以进行复制。这种接口称为「标记接口」。
Java 要求被克隆的类必须显式实现 Cloneable
接口。如果没有实现 Cloneable
接口的类的实例调用了 clone
方法, 则会在运行时抛出 CloneNotSupportedException
异常。
在实际应用开发中,浅克隆肯定不能满足所有业务场景。部分情况下,需要将浅克隆优化为深克隆,具体实现方法:实现 Cloneable
接口,重写 clone()
方法,在 clone()
方法中手动克隆引用属性。重写时记得 super.clone()
。
示例代码:
1 | public class Test { |
实际上,上面的代码克隆得还是不够彻底:
想要做到完完整整的深克隆,必须保证所有引用属性克隆后都会创建新对象,并且这个过程需要无限向下递归,直到只剩下常量属性。想要实现这种程度的深克隆几乎是不可能的,因为一旦代码中引入 SDK 包中的类,且该类没有重写 clone()
方法,就无法实现深克隆。
Serializable
:序列化后再反序列化
把对象写到流里的过程是串行化(Serilization)过程,一种形象的说法为「冷冻」或者「腌咸菜(picking)」过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做「解冻」或者「回鲜(depicking)」过程。
将对象序列化,然后再反序列化成新的对象是深拷贝的一种方法。
示例代码:
1 | package top.uuanqin; |
使用反射方法
通过 站内文章反射 生成对象,通过反射机制获取该对象的所有字段和属性信息。遍历所有字段和属性,以递归方式将源对象中的值复制到目标对象中。
本文参考
- 《图解设计模式》第 6 章
- 本科生课程笔记《程序设计中级实践&设计模式》 - TJU 🍐⚱️
- 极客时间专栏 - 设计模式之美 - 王争
- 深拷贝和浅拷贝的区别,你真的弄懂了吗? - 知乎
- 【面试题精讲】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?-腾讯云开发者社区-腾讯云
- 【不推荐】java clone() 方法详解及深克隆与浅克隆-CSDN博客
- 详解Java中的clone方法 - 何必等明天 - 博客园
- Java对象-深拷贝(实现Serializable, Cloneable两种方式)_java没有getset的深拷贝-CSDN博客
- 解读 — 深拷贝 - NiueryDiary - 博客园
- 详解Java设计模式之原型模式(Prototype Pattern)_邮件复制 java 模式-CSDN博客
- 原型模式 | 菜鸟教程
- 原型设计模式