对象复制的魔法------探索原型模式的魅力
原型模式
很简单,通过原型模式,你可以克隆出多个一模一样的对象。
1. 定义
原型模式
是使用原型实例指定创建对象的种类,并且通过克隆这些原型创建新的对象。
2. 结构
- Prototype :
抽象原型类
声明了克隆方法的接口,是具体原型类的父类 - ConcretePrototype :
具体原型类
实现了抽象原型类中声明的克隆方法,该方法返回一个自己的克隆对象 - Client :
客户类
让一个原型类克隆自身,从而创建新的对象
3. 设计原理
原型模式
的设计原理是客户端将调用原型对象的克隆
方法自己实现创建过程。原型对象的核心就是对象克隆
。
4. 案例分析
假如我们需要开发一款游戏,我们需要生成许多怪物,我们可以使用原型对象对怪物进行创建和管理。
抽象原型对象:
typescript
public abstract class Monster implements Cloneable {
private String name;
private int HP; // 生命值
public Monster(String name, int HP) {
this.name = name;
this.HP = HP;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHP() {
return HP;
}
public void setHP(int HP) {
this.HP = HP;
}
具体原型对象:
scala
public class Goblin extends Monster{
public Goblin(String name, int HP) {
super(name, HP);
}
@Override
protected Goblin clone() {
Object obj = null;
try {
obj = super.clone();
return (Goblin) obj;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
客户端:
csharp
public class Client {
public static void main(String[] args) {
// 先创建一个小妖精
Goblin goblin = new Goblin("小妖精", 100);
System.out.println(goblin.getName());
System.out.println(goblin.getHP());
// 通过原型创建一个新的妖精
Goblin newGobin = goblin.clone();
System.out.println(newGobin.getName());
System.out.println(newGobin.getHP());
// 自己给妖精设置属性
newGobin.setName("大妖精");
newGobin.setHP(500);
System.out.println("小妖精的名字:" + goblin.getName() + "; 小妖精的生命值:" + goblin.getHP());
System.out.println("新妖精的名字:" + newGobin.getName() + "; 新妖精的生命值:" + newGobin.getHP());
}
}
创建新的怪物对象时,不需要重新使用new关键字创建,直接克隆。

我们可以看到我们很快就创建了一个独立的对象,而且和之前的原型对象是独立的。
下面需要进行优化,每个怪物都有自身独有的技能,我们改造一下代码。
arduino
public class Skilledness {
private String name;
private int damage;
public Skilledness(String name, int damage) {
this.name = name;
this.damage = damage;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getDamage() {
return damage;
}
public void setDamage(int damage) {
this.damage = damage;
}
@Override
public String toString() {
return "Skilledness{" +
"name='" + name + ''' +
", damage=" + damage +
'}';
}
}
typescript
public abstract class Monster implements Cloneable {
private String name;
private int HP; // 生命值
// 新增技能属性
private Skilledness skilledness;
public Monster(String name, int HP) {
this.name = name;
this.HP = HP;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHP() {
return HP;
}
public void setHP(int HP) {
this.HP = HP;
}
public Skilledness getSkilledness() {
return skilledness;
}
public void setSkilledness(Skilledness skilledness) {
this.skilledness = skilledness;
}
}
scala
public class Orge extends Monster{
public Orge(String name, int HP) {
super(name, HP);
}
@Override
protected Orge clone() {
Object obj = null;
try {
obj = super.clone();
return (Orge) obj;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
客户端:
csharp
public class Client {
public static void main(String[] args) {
// 先创建一个恶魔
Orge orge = new Orge("大恶魔", 1000);
// 给恶魔设置一个技能
Skilledness skilledness = new Skilledness("鬼斩", 200);
orge.setSkilledness(skilledness);
// 再创建一个恶魔
Orge orge1 = orge.clone();
System.out.println("新恶魔名称:" + orge1.getName());
System.out.println("新恶魔生命值:" + orge1.getHP());
System.out.println("新恶魔技能:" + orge1.getSkilledness());
// 修改技能
orge1.getSkilledness().setDamage(300);
// 查看技能伤害值
// 为什么复制的技能伤害值改变,原来的技能伤害值也改变了?
System.out.println("原恶魔伤害:" + orge.getSkilledness().getDamage());
System.out.println("新恶魔伤害:" + orge1.getSkilledness().getDamage());
}
}

5. 深拷贝和浅拷贝
上面的代码中,我们可以看出,技能在复制时,新的技能改变,原来的技能值也改变了,这并不是我们想要的结果,这个是为什么呢?我们应该怎么实现对技能真正的复制呢?
回答上面的代码之前,需要先了解两种不同的克隆方法,分别是深拷贝
和浅拷贝
。
- 浅拷贝:创建一个新的对象,然后将原始对象的非静态字段的值赋值到新的对象,如果包含引用对象,则将引用对象的地址复制一份给克隆的对象,也就是说新的对象和原对象的成员变量指向相同的内存地址。

上面代码中都属于
浅拷贝
的实现,所以当新的技能值改变之后,原来的技能值也会发生改变。
- 深拷贝:创建一个新的对象,并且递归的复制原始对象及所有引用类型的成员变量,使得新的对象和原对象完全独立。深拷贝建创建的对象和相关的对象都是新的,不是共享同一引用。

在Java语言中,
深拷贝
的实现可以考虑使用序列化
等方式。通过将对象序列化成字节流,然后再将字节流反序列化为新的对象。下面是实现对技能的深拷贝实现。
因为需要实现序列化,所以怪物类和技能类都需要实现Serializable
接口,这里就不单独展示了。
scala
public class Orge extends Monster {
public Orge(String name, int HP) {
super(name, HP);
}
protected Orge deepClone() throws IOException, ClassNotFoundException {
// 序列化为字节流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
// 反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (Orge) objectInputStream.readObject();
}
}
csharp
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 先创建一个恶魔
Orge orge = new Orge("大恶魔", 1000);
// 给恶魔设置一个技能
Skilledness skilledness = new Skilledness("鬼斩", 200);
orge.setSkilledness(skilledness);
// 再创建一个恶魔
Orge orge1 = orge.deepClone();
System.out.println("新恶魔名称:" + orge1.getName());
System.out.println("新恶魔生命值:" + orge1.getHP());
System.out.println("新恶魔技能:" + orge1.getSkilledness());
// 修改技能
orge1.getSkilledness().setDamage(300);
// 查看技能伤害值
// 新恶魔伤害值修改了,但是原恶魔伤害值没有修改,深拷贝成功
System.out.println("原恶魔伤害:" + orge.getSkilledness().getDamage());
System.out.println("新恶魔伤害:" + orge1.getSkilledness().getDamage());
}
}

6.UML图

7. 原型管理器
原型管理器
是将多个原型对象存储在一个集合中供客户端使用,它是专门负责创建对象的工厂。
typescript
public class MonsterManager {
private static Map<String, Monster> monsterMap = new HashMap<>();
static {
monsterMap.put("Goblin", new Goblin("Small Goblin", 100));
monsterMap.put("Orge", new Goblin("Big Goblin", 1000));
}
public static Monster getMonster(String type) {
Monster monster = monsterMap.get(type);
return monster != null ? monster.clone() : null;
}
public static void addMonster(String type, Monster monster) {
monsterMap.put(type, monster);
}
}
在上述代码中引入了原型管理器,创建怪物时只需要使用管理器来进行创建即可,管理器中也是使用克隆的方式来创建的。
8. 优缺点
8.1 优点
- 性能提高: 克隆对象比直接创建对象的性能更好,通过复制现有对象,避免初始化对象的步骤;
- 扩展性好: 由于在原型模式中引入了抽象原型类,可以针对抽象进行编程,可以实现对具体原型类的扩展;(符合依赖导致)
- 状态保存: 可以使用深拷贝的方法保存对象的状态,使用原型模式将对象复制一份并将其保存,可以方便后续恢复到某一历史状态;
8.2 缺点
- 克隆实现困难: 每一个类都需要重写克隆方法,当修改克隆逻辑时,需要修改已有代码;(违反开闭原则)
- 负责对象处理困难: 如果对象包含循环引用或者其他负责结构时,需要考虑拷贝方式,需要注意考虑深拷贝和浅拷贝的问题;
9. 使用场景
- 对象创建成本高: 如果创建一个对象需要占用太多的资源,可以使用原型模式,避免了初始化对象所需的大部分步骤,提高性能;
- 类实例之间区别小: 如果一个类的实例之间区别较小,通过复制已有实例的数据创建新的实例,而不是通过构造函数初始化;
- 大量相似对象的创建: 在需要创建大量相似对象的情况下,原型模式可以通过复制原型对象来生成大量对象,避免了重复的初始化过程;