一、简介
原型模式(Prototype Pattern) 是一种创建型设计模式,它允许通过复制现有对象来创建新对象,而无需向客户端暴露对象创建的细节。这种模式涉及使用原型实例作为要创建对象的种类,并通过复制这些原型来创建新的对象。
在原型模式中,客户端通过请求复制(克隆)原型实例来创建一个新的对象,而不是通过标准的对象构造方法。这使得创建新对象更加高效,因为它避免了创建对象的常规创建过程,如实例化和初始化。在实现上,原型模式通常需要目标类实现一个能够克隆自身的接口或方法(例如clone()方法),以便能够通过复制现有对象来生成新对象。
当涉及到对象的创建时,原型模式允许我们通过复制现有对象来创建新对象。原型模式一般是通过深浅拷贝来实现的。我们假设有个动物类 Animal 和食物类 Food
Animal.java
package com.alian.prototype;
import java.io.Serializable;
public abstract class Animal implements Serializable {
public String name;
public String sex;
public abstract void sleep();
public Animal() {
}
public Animal(String name, String sex) {
this.name = name;
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
Food.java
package com.alian.prototype;
import java.io.Serializable;
public class Food implements Serializable {
private String name;
private int num;
public Food() {
}
public Food(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
接下来我们看看没有使用原型模式,和使用原型模式的区别。
二、原型模式
2.1、不使用原型模式
假设我们有一个狗类 Dog
Dog.java
package com.alian.prototype;
import java.io.Serializable;
public class Dog extends Animal implements Serializable {
private int leg;
private String breeds;
public Dog() {
}
public Dog(String name, String sex, int leg, String breeds) {
super(name, sex);
this.leg = leg;
this.breeds = breeds;
}
public int getLeg() {
return leg;
}
public void setLeg(int leg) {
this.leg = leg;
}
public String getBreeds() {
return breeds;
}
public void setBreeds(String breeds) {
this.breeds = breeds;
}
@Override
public void sleep() {
System.out.println("睡觉要吹空调");
}
public static void main(String[] args) {
Dog chineseRuralDog = new Dog("大毛", "雄性", 4, "中华田园犬");
System.out.println(chineseRuralDog);
// 如果需要创建新的用户对象,需要再次使用构造函数创建
Dog labrador = new Dog("二毛", "雄性", 4, "拉布拉多");
System.out.println(labrador);
}
}
没有使用原型的时候每次都需要 new 一个对象来实现。
2.2、使用原型模式(浅拷贝)
实现浅拷贝的一个简单方式就是实现了 Cloneable 接口,并重写了 clone() 方法。我们用一个猫类 Cat 来说明浅拷贝。
package com.alian.prototype;
import java.io.Serializable;
public class Cat extends Animal implements Cloneable, Serializable {
private int leg;
private String breeds;
private Food food;
public Cat() {
}
public Cat(String name, String sex, int leg, String breeds) {
super(name, sex);
this.leg = leg;
this.breeds = breeds;
}
public int getLeg() {
return leg;
}
public void setLeg(int leg) {
this.leg = leg;
}
public String getBreeds() {
return breeds;
}
public void setBreeds(String breeds) {
this.breeds = breeds;
}
public Food getFood() {
return food;
}
public void setFood(Food food) {
this.food = food;
}
@Override
public void sleep() {
System.out.println("睡觉要吹空调");
}
@Override
public Cat clone() throws CloneNotSupportedException {
return (Cat) super.clone();
}
public static void main(String[] args) {
Cat cat = new Cat("三毛", "雌性", 4, "金吉拉猫");
cat.setFood(new Food("鱼", 2));
System.out.println("------------------------原对象属性-------------------------");
System.out.println("原对象名称:" + cat.getName());
System.out.println("原对象性别:" + cat.getSex());
System.out.println("原对象脚的数量:" + cat.getLeg());
System.out.println("原对象品种:" + cat.getBreeds());
System.out.println("原对象口粮:" + cat.getFood().getName());
System.out.println("原对象数量:" + cat.getFood().getNum());
// 如果需要创建新的用户对象,需要再次使用构造函数创建
try {
// 使用原型模式创建新的用户对象,通过克隆方法复制现有用户对象
Cat newCat = cat.clone();
newCat.setName("四毛");
newCat.setBreeds("曼基康猫");
Food food2 = newCat.getFood();
food2.setName("猫粮");
food2.setNum(3);
System.out.println("------------------------克隆对象属性-------------------------");
System.out.println("克隆对象名称:" + newCat.getName());
System.out.println("克隆对象性别:" + newCat.getSex());
System.out.println("克隆对象脚的数量:" + newCat.getLeg());
System.out.println("克隆对象品种:" + newCat.getBreeds());
System.out.println("克隆对象口粮:" + newCat.getFood().getName());
System.out.println("克隆对象数量:" + newCat.getFood().getNum());
System.out.println("------------------------原对象属性-------------------------");
System.out.println("原对象名称:" + cat.getName());
System.out.println("原对象性别:" + cat.getSex());
System.out.println("原对象脚的数量:" + cat.getLeg());
System.out.println("原对象品种:" + cat.getBreeds());
System.out.println("原对象口粮:" + cat.getFood().getName());
System.out.println("原对象数量:" + cat.getFood().getNum());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
------------------------原对象属性-------------------------
原对象名称:三毛
原对象性别:雌性
原对象脚的数量:4
原对象品种:金吉拉猫
原对象口粮:鱼
原对象数量:2
------------------------克隆对象属性-------------------------
克隆对象名称:四毛
克隆对象性别:雌性
克隆对象脚的数量:4
克隆对象品种:曼基康猫
克隆对象口粮:猫粮
克隆对象数量:3
------------------------原对象属性-------------------------
原对象名称:三毛
原对象性别:雌性
原对象脚的数量:4
原对象品种:金吉拉猫
原对象口粮:猫粮
原对象数量:3
通过运行结果我们看到调用 clone() 方法后,然后重新设置了 Food 对象的属性,然后就发现拷贝对象是变了,同时也影响到了原对象的属性 Food (原对象的口粮从 鱼 变成 猫粮 ,数量 2 份变成 3 份)。这是为什么呢?
在Java语言中, clone() 方法执行的是浅拷贝,只会复制对象的基本数据类型(如int,String等,本例中就都没有改变),以及所引用的对象的内存地址,比如我们这里的 Food 的内存地址( 因为改变拷贝对象结果原对象也变了 ),不会递归地复制所引用的对象本身。
2.3、使用原型模式(深拷贝)
我们还是用上面的示例,不过一定要注意的是深拷贝一般父子类都要实现序列化接口 Serializable 。我们利用字节输入输出流来实现,代码如下:
CloneObject.java
package com.alian.prototype;
import java.io.*;
public class CloneObject {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Cat cat = new Cat("三毛", "雌性", 4, "金吉拉猫");
cat.setFood(new Food("鱼", 2));
System.out.println("------------------------原对象属性-------------------------");
System.out.println("原对象名称:" + cat.getName());
System.out.println("原对象性别:" + cat.getSex());
System.out.println("原对象脚的数量:" + cat.getLeg());
System.out.println("原对象品种:" + cat.getBreeds());
System.out.println("原对象口粮:" + cat.getFood().getName());
System.out.println("原对象数量:" + cat.getFood().getNum());
// 如果需要创建新的用户对象,需要再次使用构造函数创建
// 使用原型模式创建新的用户对象,通过克隆方法复制现有用户对象
Cat newCat = clone(cat);
newCat.setName("四毛");
newCat.setBreeds("曼基康猫");
Food food2 = newCat.getFood();
food2.setName("猫粮");
food2.setNum(3);
System.out.println("------------------------克隆对象属性-------------------------");
System.out.println("克隆对象名称:" + newCat.getName());
System.out.println("克隆对象性别:" + newCat.getSex());
System.out.println("克隆对象脚的数量:" + newCat.getLeg());
System.out.println("克隆对象品种:" + newCat.getBreeds());
System.out.println("克隆对象口粮:" + newCat.getFood().getName());
System.out.println("克隆对象数量:" + newCat.getFood().getNum());
System.out.println("------------------------原对象属性-------------------------");
System.out.println("原对象名称:" + cat.getName());
System.out.println("原对象性别:" + cat.getSex());
System.out.println("原对象脚的数量:" + cat.getLeg());
System.out.println("原对象品种:" + cat.getBreeds());
System.out.println("原对象口粮:" + cat.getFood().getName());
System.out.println("原对象数量:" + cat.getFood().getNum());
}
private static <T extends Serializable> T clone(T obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (T) ois.readObject();
}
}
运行结果:
------------------------原对象属性-------------------------
原对象名称:三毛
原对象性别:雌性
原对象脚的数量:4
原对象品种:金吉拉猫
原对象口粮:鱼
原对象数量:2
------------------------克隆对象属性-------------------------
克隆对象名称:四毛
克隆对象性别:雌性
克隆对象脚的数量:4
克隆对象品种:曼基康猫
克隆对象口粮:猫粮
克隆对象数量:3
------------------------原对象属性-------------------------
原对象名称:三毛
原对象性别:雌性
原对象脚的数量:4
原对象品种:金吉拉猫
原对象口粮:鱼
原对象数量:2
从结果看到深拷贝对象的属性完美的复制的,并且也没有影响原对象的属性,但是深拷贝明显要比浅拷耗时多,耗内存多。
三、原型模式
原型模式的优点和缺点如下:
优点:
- 对象复制: 允许在运行时复制现有对象,无需知道其具体实现。它可以在不影响客户端的情况下生成新对象。
- 减少资源消耗: 避免了对象的重新创建,提高了性能,特别是当创建对象的成本很高时。
- 简化对象创建: 通过克隆现有实例来创建新对象,避免了复杂的初始化步骤。适用于对象的初始化操作比较复杂的情况。
缺点:
- 深拷贝实现困难: 当对象中含有引用类型属性时,需要进行深拷贝才能保证拷贝对象与原始对象的属性完全独立。实现深拷贝可能比较困难。
- 破坏封装性: 原型模式需要对象实现克隆接口,这可能暴露对象的细节,破坏封装性。
- 需要理解对象的结构: 使用原型模式需要了解对象的内部结构,因为克隆会直接拷贝对象的字段。
- 可能产生多个相似对象: 如果拷贝出的对象过多,导致内存消耗增加。需要谨慎使用,避免滥用导致系统资源浪费。
原型模式适用于需要创建大量相似对象且创建过程复杂的场景,通过复制现有对象来提高效率。但在使用时需要注意深拷贝、克隆方式选择等问题,以避免潜在的性能问题和对象状态的不一致。