Java设计模式-创建型设计模式-原型模式
从这一专栏开始将学习设计模式,上课学习和自己总结归纳的笔记将总结出来供大家参考。
参考书籍:《设计模式就该这样学》
其他文章:
文章目录
一、创建型设计模式
创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离,对客户端代码需要调用对象的时候隐藏了类的实例化的创建细节。
其中包括:简单工厂模式(不在GoF23种设计模式中)、工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式。
二、原型模式
1.原型模式定义
用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。说大白话就是自己复制自己,通过原生对象复制出一个新的对象,这两个对象结构相同且相似,但不是通过new出来的(不调用构造函数)。
需要注意的是,原型对象自己不仅是个对象还是个工厂。并且通过克隆方式创建的对象是全新的对象,它们都是有自己的新的地址,通常对克隆模式所产生的新对象进行修改,是不会对原型对象造成任何影响的,每一个克隆对象都是相对独立的,通过不同的方式对克隆对象进行修改后,可以的到一系列相似但不完全相同的对象。
原型模式我们也称为克隆模式,即一个某个对象为原型克隆出来一个一模一样的对象,该对象的属性和原型对象一模一样。而且对于原型对象没有任何影响。原型模式的克隆方式有两种:浅克隆和深克隆。
在GoF23种设计模式中属于创建型设计模式:
其中包括:简单工厂模式(不在GoF23种设计模式中)、工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式。
2.浅克隆和深克隆
浅克隆:复制对象时仅仅复制对象本身,包括基本属性,但该对象的属性引用其他对象时,该引用对象不会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个。
深克隆:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深克隆把要复制的对象所引用的对象都复制了一遍。
3.原型模式的角色:
抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类。
具体原型类(ConcretePrototype):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
客户类(Client):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
4.原型模式的特点
优点:
- 简化对象的创建过程,通过复制一个已有对象实例可以提高新实例的创建效率
- 扩展性好
- 提供了简化的创建结构,原型模式中的产品的复制是通过封装在原型类中的克隆方法实现的,无需专门的工厂类来创建产品
- 可以通过深克隆的方式保存对象的状态,使用原型模式将对象复制一份并其状态保存起来,以便在需要的时候使用,可辅助实现撤销操作
缺点:
- 需要为每一个类准备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有类进行改造时,需要修改原代码,违背了开闭原则
- 在实现深克隆时需要写较复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类必须支持深克隆,实现起来较繁琐.
适用环境:
- 创建新对象成本较大,新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量修改
- 系统要保存对象的状态,而对象的状态变化很小
- 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更方便
5.原型模式的类图
6.原型模式的代码实现
6.1Java中如何实现对象的克隆
在Java中可以直接使用Object提供的clone方法实现对象的克隆。但需要注意的是,能够实现克隆的java类必须实现一个标识接口:Cloneable,标识这个Java类支持复制。如果一个类没有实现这个接口而调用了clone方法,java编译器将抛出一个CloneNotSupportedException异常。
Clone方法的条件:
1.对于任何对象x,有x.clone()!=x
2.对于任何对象x,有x.clone().getClass()==x.getClass()
3.如果x的equals方法定义恰当,那么x.clone().equals(x)应当成立
6.2具体案例:
类图如下:
6.2.1浅克隆具体代码实现:
1.附件类Attachment
/**
* 附件类
*/
public class Attachment {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void download(){
System.out.println("下载附件,文件名为:" + this.name);
}
}
2.原型类WeeklyLog
/**
* WeeklyLog原型类
* @author WxrStart
* @create 2022-04-12 16:43
*/
public class WeeklyLog implements Cloneable{
private Attachment attachment;
private String name;
private Date date;
private String content;
public Attachment getAttachment() {
return attachment;
}
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
//使用clone()方法来实现浅克隆
public WeeklyLog clone(){
Object o = null;
try {
o = super.clone();
return (WeeklyLog) o;
} catch (CloneNotSupportedException e) {
System.out.println("不支持复制");
e.printStackTrace();
}
return null;
}
}
3.客户端测试类
**
* 客户端测试类
* @author WxrStart
* @create 2022-04-12 16:45
*/
public class Client {
public static void main(String[] args) {
WeeklyLog preLog,newLog;
Attachment attachment = new Attachment();
attachment.setName("week 1 attachment");
preLog = new WeeklyLog();
preLog.setContent("周报week1的内容");
preLog.setDate(new Date(System.currentTimeMillis()-7*24*3600*1000));
preLog.setName("周报week1");
preLog.setAttachment(attachment);
//浅克隆新的对象newLog
newLog = preLog.clone();
//这里就是判断通过克隆方式创建的对象是否是全新的对象,false为全新对象
System.out.println("周报地址(克隆的对象地址)是否相同?" + (preLog == newLog));
System.out.println("*********************************************************");
//通过下面几行代码,证实浅拷贝的特点
System.out.println("preLog和newLog的未更改之前的Attachment的Name");
System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
//给newLog设置新的Attachment对象的属性Name
newLog.getAttachment().setName("week 2 attachment");
System.out.println("preLog和newLog的更改之后的Attachment的Name");
System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
System.out.println("*********************************************************");
System.out.println("preLog和newLog的未更改之前的date属性");
System.out.println("preLog的date: "+preLog.getDate());
System.out.println("newLog的date: "+newLog.getDate());
//给newLog设置新的Attachment对象的属性Name
newLog.setDate(new Date());
System.out.println("preLog和newLog的更改之后的date属性");
System.out.println("preLog的date: "+preLog.getDate());
System.out.println("newLog的date: "+newLog.getDate());
}
}
测试结果:
很显然,浅拷贝成功了
1.被preLog克隆出来的newLog对象地址不一样。
2.复制对象时仅仅复制对象本身,包括基本属性(Date),但该对象的属性引用其他对象时(Attachment),该引用对象不会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个。所以Date修改后preLog和newLog的Date值不一样,但是Attachment的name值修改后会一样。
6.2.2深克隆具体代码实现:
很显然,在clone方法的时候有问题了,所以我们需要重新修改clone方法,但是如果还用clone的话很难做到,所以我们选择使用Java自带的序列化机制
1.附件类Attachment
/**
* 附件类
*/
public class Attachment implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void download() {
System.out.println("下载附件,文件名为:" + this.name);
}
}
2.原型类WeeklyLog
/**
* WeeklyLog原型类
* @author WxrStart
* @create 2022-04-12 16:43
*/
public class WeeklyLog implements Serializable{
private Attachment attachment;
private String name;
private Date date;
private String content;
public Attachment getAttachment() {
return attachment;
}
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
//使用序列化技术实现深克隆
public WeeklyLog deepClone() throws Exception{
//1.创建一个字节数组输出流
ByteArrayOutputStream bos=new ByteArrayOutputStream();
//2.用对象输入流包装
ObjectOutputStream oos=new ObjectOutputStream(bos);
//3.将调用该方法的对象写出
oos.writeObject(this);
//4.创建一个字节数组输入流,读取刚刚输出到字节数组的对象this
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
//5.用对象输入流包装
ObjectInputStream ois=new ObjectInputStream(bis);
//6.返回深克隆对象
return (WeeklyLog)ois.readObject;
}
}
3.客户端测试类
**
* 客户端测试类
* @author WxrStart
* @create 2022-04-12 16:45
*/
public class Client {
public static void main(String[] args) throws Exception {
WeeklyLog preLog,newLog;
Attachment attachment = new Attachment();
attachment.setName("week 1 attachment");
preLog = new WeeklyLog();
preLog.setContent("周报week1的内容");
preLog.setDate(new Date(System.currentTimeMillis()-7*24*3600*1000));
preLog.setName("周报week1");
preLog.setAttachment(attachment);
//浅克隆新的对象newLog
newLog = preLog.deepClone();
//这里就是判断通过克隆方式创建的对象是否是全新的对象,false为全新对象
System.out.println("周报地址(克隆的对象地址)是否相同?" + (preLog == newLog));
System.out.println("*********************************************************");
//通过下面几行代码,证实浅拷贝的特点
System.out.println("preLog和newLog的未更改之前的Attachment的Name");
System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
//给newLog设置新的Attachment对象的属性Name
newLog.getAttachment().setName("week 2 attachment");
System.out.println("preLog和newLog的更改之后的Attachment的Name");
System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
System.out.println("*********************************************************");
System.out.println("preLog和newLog的未更改之前的date属性");
System.out.println("preLog的date: "+preLog.getDate());
System.out.println("newLog的date: "+newLog.getDate());
//给newLog设置新的Attachment对象的属性Name
newLog.setDate(new Date());
System.out.println("preLog和newLog的更改之后的date属性");
System.out.println("preLog的date: "+preLog.getDate());
System.out.println("newLog的date: "+newLog.getDate());
}
}
测试结果:
很显然,深拷贝成功了
1.被preLog克隆出来的newLog对象地址不一样。
2.复制对象时不光复制对象本身,包括基本属性(Date)和该对象的属性引用其他对象 (Attachment),该引用对象也会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象不是同一个。所以Date修改后preLog和newLog的Date值不一样,但是Attachment的name值修改后也会不一样。