原型模式介绍
原型模式(Prototype Pattern)是创建型模式的一种。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。
原型模式定义
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
原型模式使用场景
- 如果类的初始化需要耗费较多的资源,那么可以通过原型拷贝避免这些消耗。
- 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以拷贝多个对象供调用者使用,即保护性拷贝。
原型模式 UML 类图
角色介绍:
- Client:客户端角色。
- Prototype:抽象原型角色,抽象类或者接口,用来声明clone方法。
- ConcretePrototype:具体的原型类,是客户端角色使用的对象,即被复制的对象。
需要注意的是,Prototype 通常是不用自己定义的,因为拷贝这个操作十分常用,Java 中就提供了Cloneable 接口来支持拷贝操作,它就是原型模式中的Prototype。当然,原型模式也未必非得去实现Cloneable接口,也有其他的实现方式。
原型模式的实现
原型模式的核心是clone方法,通过该方法进行拷贝,这里举一个名片拷贝的例子。
现在已经流行电子名片了,只要扫一下就可以将名片拷贝到自己的名片库中, 我们先实现名片类。
Java 原型类就是 Cloneable 了
具体原型类
public class BusinessCard {
private String name;
private String company;
public BusinessCard() {
System.out.println("执行构造函数BusinessCard");
}
public void setName(String name) {
this.name = name;
}
public void setCompany(String company) {
this.company = company;
}
@Override
public BusinessCard clone() {
BusinessCard businessCard = null;
try {
businessCard = (BusinessCard) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return businessCard;
}
public void show() {
System.out.println("name:" + name + ", company:" + company);
}
}
注意: Cloneable 是一个空接口,clone 方法是在 Object 类中定义,访问类型为 protected,当执行 clone 方法时会检测是不是 Cloneable 类型,如果不是就会报java.lang.CloneNotSupportedException
错误。
客户端
public class Client {
public static void main(String[] args) {
BusinessCard businessCard = new BusinessCard();
businessCard.setName("小明");
businessCard.setCompany("百度");
BusinessCard cloneCard1 = businessCard.clone();
cloneCard1.setName("小强");
cloneCard1.setCompany("阿里");
BusinessCard cloneCard2 = businessCard.clone();
cloneCard2.setName("小红");
cloneCard2.setCompany("腾讯");
businessCard.show();
cloneCard1.show();
cloneCard2.show();
}
}
除了第一个名片,其他两个名片都是通过clone方法得到的,需要注意的是,clone方法返回的对象是在内存中二进制拷贝的原对象,并不会执行其构造方法。运行结果为:
执行构造函数BusinessCard
name:小明, company:百度
name:小强, company:阿里
name:小红, company:腾讯
浅拷贝和深拷贝
原型模式涉及到浅拷贝和深拷贝的知识点,为了更好的理解它们,还需要举一些例子。
实现浅拷贝
上述的例子中,BusinessCard 的字段都是String类型的,如果字段是引用的类型的,会出现什么情况呢?如下所示:
public class BusinessCard implements Cloneable {
private String name;
private Company company = new Company();
public BusinessCard() {
System.out.println("执行构造函数BusinessCard");
}
public void setName(String name) {
this.name = name;
}
public void setCompany(String name, String address) {
company.name = name;
company.address = address;
}
@Override
public BusinessCard clone() {
BusinessCard businessCard = null;
try {
businessCard = (BusinessCard) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return businessCard;
}
public void show() {
System.out.println("name:" + name + ", company:" + company);
}
}
public class Company {
String name;
String address;
@Override
public String toString() {
return "Company{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
客户端代码
public class Client {
public static void main(String[] args) {
BusinessCard businessCard = new BusinessCard();
businessCard.setName("小明");
businessCard.setCompany("百度", "北京");
BusinessCard cloneCard1 = businessCard.clone();
cloneCard1.setName("小强");
cloneCard1.setCompany("阿里", "杭州");
BusinessCard cloneCard2 = businessCard.clone();
cloneCard2.setName("小红");
cloneCard2.setCompany("腾讯", "深圳");
businessCard.show();
cloneCard1.show();
cloneCard2.show();
}
}
执行结果:
执行构造函数BusinessCard
name:小明, company:Company{name='腾讯', address='深圳'}
name:小强, company:Company{name='腾讯', address='深圳'}
name:小红, company:Company{name='腾讯', address='深圳'}
从结果可以看出 company 字段为最后设置的”腾讯”、”深圳”。这是因为 Object 类提供的 clone 方法,不会执行对象中的内部数组和引用对象的 clone 方法,导致 clone 出的对象的 company 指向的对象仍旧是原型中的 company 指向的对象。这种拷贝叫做浅拷贝。
浅拷贝显然不符号要求,实际开发时我们希望每个对象可以独立变化,此时我们就需要通过深拷贝来解决,也就是执行 clone 方法时,也需要执行内部对象的 clone 方法。
实现深拷贝
首先需要让 Company 支持 clone,修改代码如下:
public class Company implements Cloneable {
String name;
String address;
@Override
public Company clone() {
Company company = null;
try {
company = (Company) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return company;
}
@Override
public String toString() {
return "Company{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
修改 BusinessCard 的 clone 方法
public class BusinessCard implements Cloneable {
...
@Override
public BusinessCard clone() {
BusinessCard businessCard = null;
try {
businessCard = (BusinessCard) super.clone();
businessCard.company = company.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return businessCard;
}
}
Client 不变,运行结果如下:
执行构造函数BusinessCard
name:小明, company:Company{name='百度', address='北京'}
name:小强, company:Company{name='阿里', address='杭州'}
name:小红, company:Company{name='腾讯', address='深圳'}
总结
原型模式比较简单,核心问题就是对原始对象进行拷贝,需要注意的就是深、浅拷贝的问题。开发中,为了避免操作副本时影响原始对象,应尽量采用深拷贝。
优点
原型模式是在内存中二进制流的拷贝,要比 new 一个对象的性能要好,特别是需要产生大量对象时。
缺点
直接在内存中拷贝,构造函数是不会执行的,这样就减少了约束,这既是优点也是缺点,需要在实际应用中去考量。
Android 源码中的原型模式
Android 源码中的常用的 Intent 就实现了 clone 方法。
public class Intent implements Parcelable, Cloneable {
public Intent(Intent o) {
this(o, COPY_MODE_ALL);
}
private Intent(Intent o, @CopyMode int copyMode) {
this.mAction = o.mAction;
this.mData = o.mData;
this.mType = o.mType;
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
if (o.mCategories != null) {
this.mCategories = new ArraySet<>(o.mCategories);
}
if (copyMode != COPY_MODE_FILTER) {
this.mFlags = o.mFlags;
this.mContentUserHint = o.mContentUserHint;
this.mLaunchToken = o.mLaunchToken;
if (o.mSourceBounds != null) {
this.mSourceBounds = new Rect(o.mSourceBounds);
}
if (o.mSelector != null) {
this.mSelector = new Intent(o.mSelector);
}
if (copyMode != COPY_MODE_HISTORY) {
if (o.mExtras != null) {
this.mExtras = new Bundle(o.mExtras);
}
if (o.mClipData != null) {
this.mClipData = new ClipData(o.mClipData);
}
} else {
if (o.mExtras != null && !o.mExtras.maybeIsEmpty()) {
this.mExtras = Bundle.STRIPPED;
}
}
}
}
@Override
public Object clone() {
return new Intent(this);
}
...
}
可以看到 clone 方法实际上内部没有调用 super.clone() 方法来实现对象拷贝,而是调用了 new Intent(this)
。可以看到 Intent 中有很多参数,使用 clone 即可增加构建对象的便捷性,在 clone 中使用 new,可以实现定制化拷贝对象,如果使用 super.clone() 则会将原对象进行拷贝。
使用 new 和 clone 需要根据构造对象的成本来决定,如果对象的构造成本比较高或者比较麻烦,那么使用 clone 方法效率更高,否则就使用 new。