1.前言
原型模式是创建型设计模式之一。本来已经决定不写相关部分的内容,因为其内容并不是很难。
后来想了一下,里面涉及到的思考点和细节点比较多,还是应该总结一下。其次,设计模式本身可能并不会很难,哪怕是对花点时间多去看一些例子,总归能看懂,但是设计模式之难不在其本身,难在其应用场景,你可能知道它是怎样实现的,但是你不知道它应该在什么场合下被使用。使用好了它有益于系统,而滥用则会为系统增加复杂度和冗余度,它考察编程人员举一反三的能力和设计思维。
此外,原型模式也算是相对比较常见常用的设计模式之一。
基于此,还是将其总结于此,权当是对自己认知的总结,也作为督促自己的一部分。
2.从需求说起
本博客写于2020年5月,新型冠状病毒还是当下重中之重。假设现在你住在某省某市某小区xx东xx单元xx号,需要每天通过网络填写一份表单向当地社区报备体温情况。表单里需要填写的内容包括:身份证号、姓名、住址、温度。
下面开始实现。
(1)表单结构,地址是一个对象,包含省、市、街道和详细地址。
package prototype;
public class Report {
String id;
String name;
double temperature;
Address adress;
//getter和setter、toString省略
class Address{
String province;
String city;
String road;
String detail;
//getter和setter、toString省略
}
}
(2)开始填写表单。此处假设打印表单对象即为提交。
package prototype;
import java.util.Date;
import prototype.Report.Address;
public class Test1 {
public static void main(String[] args) {
//第一天填写
Report first = new Report();
first.setId("310……");
first.setName("Dolphin");
Address address1 = first.new Address();
address1.setProvince("SH");
address1.setCity("SH");
address1.setRoad("Rd Nandan");
address1.setDetail("xx community xx building xx unit No.xx");
first.setAdress(address1);
first.setTemperature(36.2);
System.out.println(first);
//第二天填写
Report second = new Report();
second.setId("310……");
second.setName("Dolphin");
Address address2 = first.new Address();
address1.setProvince("SH");
address1.setCity("SH");
address1.setRoad("Rd Nandan");
address1.setDetail("xx community xx building xx unit No.xx");
second.setAdress(address1);
second.setTemperature(36.3);
System.out.println(second);
//……
}
}
2.1 问题
上述实现方法有什么问题吗?挺好的啊~
从结果来看,上述实现并没有什么问题。
但是,仔细分析一下这个需求。一个人,需要向社区每日报备体温。在这个需求中,其实每天填写时需要修改的只有体温这个参数。
但是上述实现过程中,这个人其实每天都把身份证号、姓名、地址这些信息填了一遍又一遍,烦不烦?
烦!假如这个人又是一个对电子产品一窍不通的人,那每天经历这个过程可谓是太煎熬了
2.2 解决方案设想
针对上面提出的问题,有没有什么解决方案呢?
最好的解决方案就是,第一天填写了之后,能够保存相关信息,就相当于登录网页时的记录用户名和密码。
在下一次填写时,把相关信息加载出来,再把需要改的改掉就行了。
这应该算的上一种比较好的解决方式。
也许还有更好的解决方式需要去发现去思考,但是为了引出原型模式,这个解决方案是必须的。
3. 原型模式
3.1 简介
前面也说到了,原型模式是创建型设计模式之一,它是用来描述如何创建对象的,更确切的来说,结合上面这个需求,它是用来描述如何创建第二个、第三个……第N个与第一个对象很相似的对象。
从系统的角度上来说,这有点类似系统中的Ctrl+C
与Ctrl+V
,用于创建重复对象。
注意,这里说的是类似,而不是等同。具体哪里不同,后文会说。
在这里引申出一个概念:原型。用上面的例子来说,第一天的表单对象就是一个原型;用系统复制粘贴来说,你复制的东西就是原型。通过这个原型创建出第二个、第三个……第N个与原型对象很相似的对象,这就是原型模式的雏形。
原型模式的实现方法很简单。让原型对象类型实现Cloneable
接口,并重写Object.clone()
方法即可。
听起来是不是很简单,那就一步步实现吧。
3.1 原型模式初体验——浅拷贝
package prototype;
public class Report implements Cloneable{
String id;
String name;
double temperature;
Address adress;
//getter和setter、toString省略
//建议把protected改成public,使得可以在包外使用。也可以使用多态中的协变将返回Object修改为返回所需克隆的对象(此例中即为Report)
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
class Address{
String province;
String city;
String road;
String detail;
//getter和setter、toString省略
}
}
package prototype;
import prototype.Report.Address;
public class Test2 {
public static void main(String[] args) throws CloneNotSupportedException {
Report first = new Report();
first.setId("310……");
first.setName("Dolphin");
Address address1 = first.new Address();
address1.setProvince("SH");
address1.setCity("SH");
address1.setRoad("Rd Nandan");
address1.setDetail("xx community xx building xx unit No.xx");
first.setAdress(address1);
first.setTemperature(36.2);
System.out.println(first);
Report second = (Report) first.clone();
second.setTemperature(36.3);
System.out.println(second);
}
}
输出
Report [id=310……, name=Dolphin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
可以看到除了温度有所变化之外(用黄色标了出来),其余均没有变化。通过克隆原型对象,一些重复的信息就不需要一而再再而三的填了。这就是原型设计模式的好处,其功能类似于前文所说的把原型对象保存起来,后面需要用的时候再加载。
3.2 思考
从面向对象的角度来看,我们需要弄清楚几个问题:
- 既然是克隆,那么原型对象和克隆出来的对象是不是同一个对象?
- 修改其中之一,会不会影响到另外一个?
首先,验证第一个问题。输出两个对象的散列码,看看两者是否相等;也可以直接判断二者是否相等。
System.out.println(first.hashCode());
System.out.println(second.hashCode());
System.out.println(first == second);
输出
2018699554
1311053135
false
从输出可以看到,不管是散列码还是判断相等,都说明原型对象和克隆出来的对象并不属于同一个,而是指向了不同的地址空间。
再验证第二个问题。
//原型对象创建过程略
System.out.println("---------------------------unmodified------------------------------");
System.out.println("prototype: " + first);
second.setTemperature(36.3);
System.out.println("clone: " + second);
System.out.println("---------------------------prototype modified 1------------------------------");
first.setName("Thaksin");
System.out.println("prototype after prototype modified 1: " + first);
System.out.println("clone after prototype modified 1: " + second);
System.out.println("---------------------------prototype modified 2------------------------------");
first.getAdress().setCity("SSSS");
System.out.println("prototype after prototype modified 2: " + first);
System.out.println("clone after prototype modified 2: " + second);
System.out.println("---------------------------clone modified 1------------------------------");
second.setId("450……");
System.out.println("prototype after clone modified 1: " + first);
System.out.println("clone after clone modified 1: " + second);
System.out.println("---------------------------clone modified 2------------------------------");
second.getAdress().setCity("NN");
System.out.println("prototype after clone modified 2: " + first);
System.out.println("clone after clone modified 2: " + second);
输出如下(修改的字段用黄颜色标出,便于比较)
---------------------------unmodified------------------------------
prototype: Report [id=310……, name=Dolphin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 1------------------------------
prototype after prototype modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 1: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 2------------------------------
prototype after prototype modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 2: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 1------------------------------
prototype after clone modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 1: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 2------------------------------
prototype after clone modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=NN, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 2: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=NN, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
从输出中,对比上下两个对象的高亮部分,不难看出,不管是修改原型对象还是修改克隆出来的对象,当修改的字段为基本类型和String时,不会影响另外一个;而当修改的内容为类的引用(如Address address
、Date date
等)时,就会将另一个对象里的字段也修改了。
这是为什么呢?也许下面这张图可以解答这个疑惑。
看懂这张图需要懂一定的java创建对象与内存分配的知识。new Report()
生成原型对象,在堆内开辟了一块内存,该内存的首地址为0xfac
(假设),而这个原型对象中组合了Address
对象,所以在堆内存中又开辟了一块内存(假设首地址为0xaaa),则原型对象中address
这个引用指向这块内存的首地址。另外,栈中存放new Report()
这个原型对象的引用first
,该引用指向堆中0xfac
这块内存。
clone
操作 将 原型对象在堆中原样复制了一份,并分配了不同的地址(假设为0xfab
),栈中second
为复制所得的对象的引用。由于是原样复制,所以克隆对象中的address
引用同样指向0xaaa
这块内存。
clone的这种原样复制的方式叫做浅拷贝。
浅拷贝机制使得两个对象中的address
引用都指向同一块内存,因此当其中一个对象对这块内存中的信息做了更改后,由于另外一个对象也指向这个内存,所以同样也会接收到这个更改。
说到这里,说明一下前文提及的浅拷贝和系统中Ctrl+C
和Ctrl+V
的区别:在浅拷贝机制下,当对象中存在对象引用时,修改其中一个对象中引用的内容,会影响另外一个;而系统中的复制粘贴不会存在修改一个影响另外一个的情况。
3.3 改进——深拷贝
有浅拷贝,就会有深拷贝。
深拷贝,即在clone
函数中,对原型对象中的对象引用(address
)也进行拷贝,这样原型对象和克隆所得的对象中的对象引用(address
)就可以指向不同的内存区域,使得修改其中一个引用不会再引起另外一个引用被修改。
//修改Address类,让其实现Cloneable,并重写Object.clone()方法
class Address implements Cloneable{
//略……
@Override
public Object clone() throws CloneNotSupportedException {
Address d = null;
// try/catch CloneNotSupportedException
d = (Address) super.clone();
return d;
}
//修改Report中的clone函数
@Override
public Object clone() throws CloneNotSupportedException {
Report r = (Report) super.clone();
r.adress = (Address) adress.clone();//address也克隆一份
return r;
}
依然使用上一章节的测试,得到下面的输出。依然将修改的字段高亮,便于比较。
---------------------------unmodified------------------------------
prototype: Report [id=310……, name=Dolphin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 1------------------------------
prototype after prototype modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 1: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------prototype modified 2------------------------------
prototype after prototype modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after prototype modified 2: Report [id=310……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 1------------------------------
prototype after clone modified 1: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 1: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=SH, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
---------------------------clone modified 2------------------------------
prototype after clone modified 2: Report [id=310……, name=Thaksin, temperature=36.2, adress=Address [province=SH, city=SSSS, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
clone after clone modified 2: Report [id=450……, name=Dolphin, temperature=36.3, adress=Address [province=SH, city=NN, road=Rd Nandan, detail=xx community xx building xx unit No.xx]]
从输出可以看到,不管是原型对象还是克隆得到的对象,经过深拷贝后,修改address
这个引用均不会引起另一个对象中该引用的变化。这就是深拷贝的魔力。下面是深拷贝的示意图(用上一张图修修补补,哈哈~)。
对比浅拷贝而言,深拷贝的好处是不言而喻的,它使得两个对象完全独立。
秉着找茬找到底的态度,再提出一个疑问,深拷贝好是好,但是如果Report类里面不止Address
这个类,还有A、B、C、D
类,甚至ABCD
里面还有EFGH
类……的情况下,怎么办?当类的层次变得很深时,深拷贝将会显得捉襟见肘。
3.4 再改进——序列化和反序列化
既然深拷贝也不是很可靠,那么什么最可靠呢?
如题所述,序列化和反序列化,可以写到文件或者内存。建议还是写到内存再从内存读出来,因为这种方式强到爆炸。
- 让
Report
类实现Serializable
接口
public class Report implements Cloneable, Serializable{
//略……
}
- 让
Report
中组合的类(这里是Address
类)也实现Serializable
接口,不用再实现Cloneable
接口,也就不用再重写Object.clone
方法
class Address implements Serializable{
//略……
//不用重写Object.clone方法
}
Report
类中重写Object.clone()
方法,实现序列化和反序列化
@Override
public Object clone() throws CloneNotSupportedException {
Object clone = null;
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out)) {
oos.writeObject(this);
byte[] b = out.toByteArray();
try (ByteArrayInputStream in = new ByteArrayInputStream(b);
ObjectInputStream ois = new ObjectInputStream(in)) {
clone = ois.readObject();
return clone;
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return clone;
}
- 测试(测试程序还是那个测试程序,输出内容与深拷贝输出内容也一致,这里就省略了,/狗头))
4.总结
原型模式是创建型设计模式之一,也是比较常用的模式之一。原型模式有浅拷贝和深拷贝之分,浅拷贝在类中组合了其它类时,修改其中之一会影响另外一个;深拷贝有两种实现方式,当类的层次比较浅时可以尝试将组合的类也克隆一份,当类测层次比较深时,使用序列化和反序列化是比较合理的方法。
原型模式用于获取原型对象相同的对象,类似系统中的赋值粘贴。当new一个对象比较繁琐时而你又刚好有了一个对象,原型设计模式就很有用。
除了找一些资料外,也可以参考Java源代码中一些原型模式的写法,如java.util.Date、java.util.HashMap
等
5. 参考文献
- Java源代码
- 蜗牛学院 “Java 设计模式”