0x0、引言
? 周末尝试戒游,抽空啃《设计模式之美》,本文对应设计模式与范式:创建型(47),原型模式
(Prototype Pattern) ,创建型设计设计模式的最后一种。了解此模式的应用场景,深浅拷贝的区别即可,同样非常简单。
二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。
0x1、定义与使用场景
又称 克隆模式
,定义如下:
如果对象的 创建成本较大,且同一个类的不同对象间差别不大(大部分字段相同)的情况下,可以利用已有对象(原型)进行复制(或者叫拷贝、克隆)的方式来创建新的对象,以达到节省创建时间的目的。
简单点说就是以某个对象为原型,克隆出一个一模一样的对象,常见应用场景:
- 资源优化 (对象初始化需使用较多外部资源时,如IO、数据库、CPU,权限等);
- 复杂依赖 (依赖嵌套,A创建依赖B,B创建依赖C,一连串的对象get和set);
- 同一对象多个修改者 (多个对象的调用者都需要修改对象的值,克隆多个供使用其使用,保护性拷贝);
- 需保存原始对象状态 (如记录历史操作的场景,通过原型模式快速保存记录);
- 结合工厂模式使用 (定义统一的复制接口,如clone、copy,使用一个工厂来统一进行拷贝和新对象创建,然后由工厂方法提供给调用者使用)。
组成角色:
0x2、深浅拷贝
原型模式分为 浅拷贝
和 深拷贝
两种,浅拷贝只复制对象中基本数据类型和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象…而深拷贝得到的则是一份完完整整独立的对象,所以,深拷贝更耗时和耗内存空间。
注:浅拷贝得到的对象和原始对象会共享部分数据,可能出现数据被修改的风险,故更适用于不可变对象。
很多开源框架和组件中都有原型模式的相关实现,并不一定非得从零去实现浅拷贝和深拷贝。下面以Java为例,讲解其中深浅拷贝相关的姿势~
① Java中==与equals的区别:
==
:基本数据类型(int,long等) → 比较存储的值是否相等,引用型变量 → 比较所指向对象地址是否相等;- equals:不能用于比较基本数据类型,没有对equals()方法进行重写,默认比较指向的对象地址,若想比较对象内容,需自行重写此方法,做相应的判断。
② 克隆必须满足的三个条件
- 对任何对象x,都有 x.clone() != x,即不是同一对象;
- 对任何对象x,都有 x.clone().getClass == x.getClass(),即对象类型一致;
- 如果对象obj的equals()方法定义得当的话,obj.clone().equals(obj)应该是成立的(推荐,不强制);
③ Java中浅拷贝的实现方式
想被克隆的类实现 Cloneable
接口,重写 clone()
方法,验证代码示例如下(省略get、set方法):
// 引用类型
public class Money {
private String type; // 币种
public Money(String type) { this.type = type; }
@Override
public String toString() {
return "Money{" + "type='" + type + '\'' + '}';
}
}
// 原型类,实现Cloneable接口,重写clone方法
public class Assets implements Cloneable{
private int amount; // 数目
private Money money; // 币种
private String kind; // 资金种类
public Assets(int amount, Money money, String kind) {
System.out.println("执行了构造方法!");
this.amount = amount;
this.money = money;
this.kind = kind;
}
@Override
protected Object clone() {
Assets assets = null;
try {
assets = (Assets)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return assets;
}
@Override
public String toString() {
return "Assets{" + "amount=" + amount + ", money=" + money + ", kind='" + kind + '\'' + '}';
}
}
// 测试用例:
public class AssetsTest {
public static void main(String[] args) {
Assets a1 = new Assets(100, new Money("人民币"), "现金");
Assets a2 = (Assets) a1.clone();
System.out.println("a1.equals(a2) → " + a1.equals(a2));
System.out.println("a1 == a2 → " + (a1 == a2));
System.out.println("a1.getClass == a2.getClass → " + (a1.getClass() == a2.getClass()));
System.out.println("a1.getMoney() == a2.getMoney() → " + (a1.getMoney() == a2.getMoney()));
System.out.println("a1 → " + a1.toString());
System.out.println("a2 → " + a2.toString());
a1.setAmount(200);
System.out.println("修改基本类型属性后:");
System.out.println("a1 → " + a1.toString());
System.out.println("a2 → " + a2.toString());
a1.getMoney().setType("美金");
System.out.println("修改引用用类型属性后:");
System.out.println("a1 → " + a1.toString());
System.out.println("a2 → " + a2.toString());
}
}
复制代码
运行结果如下:
分析运行结果不难得出Java中浅拷贝具有如下特点:
- 执行clone方法,不会调用构造方法;
- 克隆会生成新的对象变量,但指向同一个内存地址;
- 克隆前后数据类型一致;
- 克隆时,基本数据类型属性会新建,引用类型只会生成一个新的引用变量,依旧指向同一个内存地址;
④ Java中深拷贝的两种实现方式
相比浅拷贝,深拷贝会连引用类型数据也新建,Java中实现深拷贝的两种方式:
- 引用类型也实现Cloneable接口,重写clone()方法,原型类clone()方法调用引用类类型的clone()为money对象赋值:
@Override
protected Object clone() {
Assets assets = null;
try {
assets = (Assets)super.clone();
assets.setMoney((Money)this.getMoney().clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return assets;
}
复制代码
注:深拷贝需要整个克隆涉及的对象都正确的实现clone()方法,其只能怪一个没正确实现克隆,都会导致深拷贝失败。
- 序列化,定义一个方法完成对象转二进制流和二进制流转对象,然后返回反序列化后的对象。
public Assets deepClone() {
try {
// 写入当前对象的二进制流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 读取二进制流产生新对象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Assets)ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
复制代码
0x3、加餐:四类创建型设计模式总结
-
单例模式
→ 创建全局唯一的对象; -
工厂模式
→ 创建不同但类型相关的对象(继承同一父类或接口),由给点参数决定构建那种类型的对象; -
建造者模式
→ 创建复杂对象,通过设置不同的可选参数,定制化创建不同对象; -
原型模式
→ 针对创建成本较大的对象,利用已有对象进行复制的方式创建,以此达到节省创建时间的目的;