原型模式
1)动机
通过对已有对象的复制和粘贴,创建大量相同的对象。
2)概述
1.定义
将原型对象
传给要发动创建的对象
,要发动创建的对象
通过请求原型对象
拷贝自己来实现创建过程。
注意:通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改,对原型对象不会造成任何影响。
2.结构图
原型模式中包含如下角色:
Prototype(抽象原型类):声明克隆方法的接口,是具体原型类的公共父类,可以是抽象类也可以是接口,还可以是具体实现类。
ConcretePrototype(具体原型类):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要实例化或通过工厂方法创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象,由于客户类针对抽象原型类Prototype编程,因此可以根据需要选择具体原型类。
注意:原型模式的核心在于如何实现克隆方法。
3)克隆方法解析
1.通用实现方法
在具体原型类的克隆方法中实例化一个与自身类型相同的对象将其返回,并将相关参数传入新创建的对象中,保证成员属性相同。
public interface Prototype {
Prototype clone();
}
-----------------------------------------------------------------------------------
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class ConcretePrototype1 implements Prototype {
private String attr; //成员属性
@Override
public Prototype clone(){
ConcretePrototype1 concretePrototype = new ConcretePrototype1();
concretePrototype.setAttr(this.attr);
return concretePrototype;
}
}
在客户类中创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象:
ConcretePrototype1 concretePrototype1 = new ConcretePrototype1();
System.out.println(concretePrototype1);
concretePrototype1.setAttr("通用克隆");
Prototype clonedPrototype = concretePrototype1.clone();
System.out.println(clonedPrototype);
2.Java提供的clone()方法
在 Java 中可以直接使用 Object 提供的clone()方法来实现对象的克隆。
注意:实现克隆的 Java 类必须实现 Cloneable 接口,表示这个 Java 类支持被复制。
class ConcretePrototype2 implements Cloneable,Prototype {
public Prototype clone() {
Object object = null;
try {
object = super.clone();
} catch (CloneNotSupportedException exception) {
System.err.println("Not support cloneable");
}
return (Prototype) object;
}
}
在客户端创建原型对象和克隆对象:
ConcretePrototype2 concretePrototype2 = new ConcretePrototype2();
System.out.println(concretePrototype2);
Prototype clonedPrototype = concretePrototype2.clone();
System.out.println(clonedPrototype);
Java 的 clone() 方法满足:
-
对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
-
对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
-
如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立;
利用 Object 类的 clone() 方法,步骤如下:
-
在派生类中覆盖基类的clone()方法,并声明为public;
-
在派生类的clone()方法中,调用super.clone();
-
派生类需实现Cloneable接口。
此时,Object类相当于抽象原型类,实现了Cloneable接口的类相当于具体原型类。
4)案例
使用原型模式实现工作周报的快速创建,结构图如下:
WeeklyLog 充当具体原型类,Object 类充当抽象原型类。
代码案例如下:
周报类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
class WeeklyLog implements Cloneable {
private String name;
private String date;
private String content;
public WeeklyLog clone() {
Object obj = null;
try {
obj = super.clone();
return (WeeklyLog) obj;
} catch (CloneNotSupportedException e) {
System.out.println("不支持复制!");
return null;
}
}
}
客户端类
public class Client {
public static void main(String args[]) {
WeeklyLog log_previous = new WeeklyLog(); //创建原型对象
log_previous.setName("张无忌");
log_previous.setDate("第12周");
log_previous.setContent("这周工作很忙,每天加班!");
System.out.println("****周报****");
System.out.println("周次:" + log_previous.getDate());
System.out.println("姓名:" + log_previous.getName());
System.out.println("内容:" + log_previous.getContent());
System.out.println("--------------------------------");
WeeklyLog log_new;
log_new = log_previous.clone(); //调用克隆方法创建克隆对象
log_new.setDate("第13周");
System.out.println("****周报****");
System.out.println("周次:" + log_new.getDate());
System.out.println("姓名:" + log_new.getName());
System.out.println("内容:" + log_new.getContent());
System.out.println(log_previous == log_new);
System.out.println(log_previous.getDate() == log_new.getDate());
System.out.println(log_previous.getName() == log_new.getName());
System.out.println(log_previous.getContent() == log_new.getContent());
}
}
5)浅克隆 与 深克隆
1.概述
Java 中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。
浅克隆 和 深克隆 的区别在于是否支持引用类型的成员变量的复制。
2.浅克隆
如果原型对象的成员变量是值类型,将复制一份给克隆对象;
如果原型对象的成员变量是引用类型,则将引用对象的地址复制给克隆对象,即原型对象和克隆对象的成员变量指向相同的内存地址。
Java 中,覆盖 Object 类的 clone() 方法可以实现浅克隆。
3.深克隆
无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象。
Java 中,可以通过序列化(Serialization)等方式实现深克隆。
PS:序列化是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中,通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象。
注意:实现序列化的对象必须实现Serializable接口。
代码案例如下:
附件类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.*;
@Data
@AllArgsConstructor
@NoArgsConstructor
class Attachment implements Serializable {
private String name; //附件名
public void download() {
System.out.println("下载附件,文件名为" + name);
}
}
周报类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
class WeeklyLog implements Serializable {
private Attachment attachment;
private String name;
private String date;
private String content;
public WeeklyLog deepClone() throws IOException, ClassNotFoundException{
//将对象写入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (WeeklyLog) ois.readObject();
}
}
客户端类
public class Client {
public static void main(String[] args){
WeeklyLog log_previous, log_new = null;
log_previous = new WeeklyLog(); //创建原型对象
Attachment attachment = new Attachment(); //创建附件对象
log_previous.setAttachment(attachment); //将附件添加到周报中
try {
log_new = log_previous.deepClone(); //调用深克隆方法创建克隆对象
} catch(Exception e){
System.err.println("克隆失败!");
}
//比较周报
System.out.println("周报是否相同? " + (log_previous == log_new));
//比较附件
System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));
}
}
6)原型管理器
1.概述
原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。
2.案例-公文管理器
3.代码实现如下
interface OfficialDocument extends Cloneable {
OfficialDocument clone();
void display();
}
public class FAR implements OfficialDocument {
@Override
public OfficialDocument clone() {
OfficialDocument far = null;
try {
far = (OfficialDocument) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("不支持复制!");
}
return far;
}
@Override
public void display() {
System.out.println("《可行性分析报告》");
}
}
public class SRS implements OfficialDocument {
@Override
public OfficialDocument clone() {
OfficialDocument srs = null;
try {
srs = (OfficialDocument) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("不支持复制!");
}
return srs;
}
@Override
public void display() {
System.out.println("《软件需求规格说明书》");
}
}
import java.util.HashMap;
public class PrototypeManager {
private HashMap ht = new HashMap<>();
private static PrototypeManager pm = new PrototypeManager();
private PrototypeManager() {
ht.put("far", new FAR());
ht.put("srs", new SRS());
}
public void addOfficialDocument(String key, OfficialDocument doc) {
ht.put(key, doc);
}
public OfficialDocument getOfficialDocument(String key) {
return ((OfficialDocument) ht.get(key)).clone();
}
public static PrototypeManager getPrototypeManager() {
return pm;
}
}
public class Client {
public static void main(String[] args) {
PrototypeManager pm = PrototypeManager.getPrototypeManager();
OfficialDocument doc1, doc2, doc3, doc4;
doc1 = pm.getOfficialDocument("far");
doc1.display();
doc2 = pm.getOfficialDocument("far");
doc2.display();
System.out.println(doc1 == doc2);
doc3 = pm.getOfficialDocument("srs");
doc3.display();
doc4 = pm.getOfficialDocument("srs");
doc4.display();
System.out.println(doc3 == doc4);
}
}
在 PrototypeManager 中定义了一个HashMap,使用"键值对"来存储原型对象,客户端可以通过Key(如"far"或"srs")来获取对应原型对象的克隆对象。
PrototypeManager类提供了类似工厂方法的getOfficialDocument()方法用于返回一个克隆对象。
此处将PrototypeManager设计为单例类,使用饿汉式单例实现,确保系统中有且仅有一个PrototypeManager对象,有利于节省系统资源,并可以更好地对原型管理器对象进行控制。
7)总结
1.优点
-
当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
-
扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
-
原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
-
可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
2.缺点
-
需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类改造时,需要修改源代码,违背了"开闭原则"。
-
在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
3.适用场景
-
创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
-
如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
-
需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。