![设计模式就该这样学:基于经典框架源码和真实业务场景](https://wfqqreader-1252317822.image.myqcloud.com/cover/758/33114758/b_33114758.jpg)
9.2 使用原型模式解决实际问题
9.2.1 分析JDK浅克隆API带来的问题
在Java提供的API中,不需要手动创建抽象原型接口,因为Java已经内置了Cloneable抽象原型接口,自定义的类型只需实现该接口并重写Object.clone()方法即可完成本类的复制。
通过查看JDK的源码可以发现,其实Cloneable是一个空接口。Java之所以提供Cloneable接口,只是为了在运行时通知Java虚拟机可以安全地在该类上使用clone()方法。而如果该类没有实现 Cloneable接口,则调用clone()方法会抛出 CloneNotSupportedException异常。
一般情况下,如果使用clone()方法,则需满足以下条件。
(1)对任何对象o,都有o.clone() != o。换言之,克隆对象与原型对象不是同一个对象。
(2)对任何对象o,都有o.clone().getClass() == o.getClass()。换言之,克隆对象与原型对象的类型一样。
(3)如果对象o的equals()方法定义恰当,则o.clone().equals(o)应当成立。
我们在设计自定义类的clone()方法时,应当遵守这3个条件。一般来说,这3个条件中的前2个是必需的,第3个是可选的。
下面使用Java提供的API应用来实现原型模式,代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_6.jpg?sign=1738974409-ZylETdG9befeEkIluIo7BqR8QZYk9lEx-0-c67aee00e9663b0798715b3552fd34df)
super.clone()方法直接从堆内存中以二进制流的方式进行复制,重新分配一个内存块,因此其效率很高。由于super.clone()方法基于内存复制,因此不会调用对象的构造函数,也就是不需要经历初始化过程。
在日常开发中,使用super.clone()方法并不能满足所有需求。如果类中存在引用对象属性,则原型对象与克隆对象的该属性会指向同一对象的引用。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_7.jpg?sign=1738974409-Nt9B6rc7wmKma6gHawdR5HBaOpQkcFsV-0-fb663aeff2233b5764b736dc39b97ecc)
修改客户端测试代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_8.jpg?sign=1738974409-fgGGZmJpGdS1eO78FtdQ0Kk0irWZ8ooW-0-efa5460b8d77686bc71860c1221d3729)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_9.jpg?sign=1738974409-qaDD9PzB6godktiSCpCTeaZaereVuQUU-0-409f2133b5b0f82d012966e0dc5f4ac8)
我们给克隆对象新增一个属性hobbies(爱好)之后,发现原型对象也发生了变化,这显然不符合预期。因为我们希望克隆对象和原型对象是两个独立的对象,不再有联系。从测试结果来看,应该是hobbies共用了一个内存地址,意味着复制的不是值,而是引用的地址。这样的话,如果我们修改任意一个对象中的属性值,protoType和cloneType的hobbies值都会改变。这就是我们常说的浅克隆,只是完整复制了值类型数据,没有赋值引用对象。换言之,所有的引用对象仍然指向原来的对象,显然不是我们想要的结果。那如何解决这个问题呢?
Java自带的clone()方法进行的就是浅克隆。而如果我们想进行深克隆,可以直接在super.clone()后,手动给克隆对象的相关属性分配另一块内存,不过如果当原型对象维护很多引用属性的时候,手动分配会比较烦琐。因此,在Java中,如果想完成原型对象的深克隆,则通常使用序列化(Serializable)的方式。
9.2.2 使用序列化实现深克隆
在上节的基础上继续改造,增加一个deepClone()方法。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_10.jpg?sign=1738974409-tGzbmNIxqFD0CkoaWcCnCdiP8HRNBEj0-0-6391e3aa14d813e27ba1bb6a42b7f942)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_11.jpg?sign=1738974409-FSxDyVhEGMiebZCKlrjXwtE4Ot4axMPB-0-09117176ef9b6b106e0fe492fba68ade)
客户端调用代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_12.jpg?sign=1738974409-ZLXcHZzPJp5LtEtV78FErRASEnL4fsyy-0-515c7353a58d7c71ee17eccc9fb14fb8)
运行程序,得到如下图所示的结果,与期望的结果一致。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_13.jpg?sign=1738974409-dIZ3MTSNRoLepyUbeYLsLzw85vdhGnOF-0-e89e473047cc285520b4948eefd0b4d0)
从运行结果来看,我们的确完成了深克隆。
9.2.3 还原克隆破坏单例的事故现场
假设有这样一个场景,如果复制的目标对象恰好是单例对象,那会不会使单例对象被破坏呢?当然,我们在已知的情况下肯定不会这么干,但如果发生了意外怎么办?不妨来修改一下代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_14.jpg?sign=1738974409-zvcMC0lqf1Ix5NCY86UY7JIitzU3xqtK-0-27224060e6fd8d506b01f4d15c493711)
我们把构造方法私有化,并且提供getInstance()方法。编写客户端测试代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_15.jpg?sign=1738974409-JRhdw55vREvGzc4N7vngFcxBTKtVKuG7-0-487888accc88e27ed711c5739ba5726a)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_16.jpg?sign=1738974409-czPnkWo9ZhjHrq4fbVsWPvjXmQe4eoNI-0-e250e2ddd44144554150f0cf9feb6c27)
运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_17.jpg?sign=1738974409-keOxc5vamfFg1RGWhx5u2kOGXxK3FKmv-0-7ca4ab38d5fb395837792aa3d677888c)
从运行结果来看,确实创建了两个不同的对象。实际上防止复制破坏单例对象的解决思路非常简单,禁止复制便可。要么我们的单例类不实现Cloneable接口,要么我们重写clone()方法,在clone()方法中返回单例对象即可,具体代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt011_18.jpg?sign=1738974409-NESjzBtxlAFNKgdSl0mOpLqInhBJDxXL-0-d077b0a35f88ef4eaf4606c4c6d83b72)