原型模式的核心在于何如实现克隆方法
Java 语言提供的 clone 方法
所有的 java 类都继承java.lang.Object,事实上,Object 类提供了一个 clone() 方法,可以将一个 java 对象复制一份。因此在 Java 中可以直接使用 Object 提供的 clone() 方法来实现对象的克隆,Java 语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个标识接口 Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了 clone() 方法,Java编译器将抛出一个
CloneNotSupportedException 异常。
示例代码:
@Data
public class Customer implements Cloneable{
private Integer age;
private String name;
private Address address;
public Customer clone() {
Customer customer = null;
try {
customer = (Customer) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return customer;
}
}
在客户端创建克隆对象也很简单:
Customer customer1 = new Customer();
Customer customer2 = customer1.clone();
一般而言,Java 对象中的 clone() 方法满足:
- 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
- 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一 样;
- 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
@Test
public void testCustomerClone() {
Address address = new Address();
address.setName("ChengDu");
Customer customer1 = new Customer();
customer1.setAge(23);
customer1.setName("Tom");
customer1.setAddress(address);
Customer customer2 = customer1.clone();
System.out.println(customer1 == customer2); //false
System.out.println(customer1.getAddress() == customer2.getAddress()); //true
customer1.getAddress().setName("HangZhou");
System.out.println(customer1.getAddress().getName()); //HangZhou
System.out.println(customer2.getAddress().getName()); //HangZhou
}
分析上面的代码,customer2 是通过 customer1 克隆得到的,并且 Customer 对象中存在 Address 属性,在浅克隆中,只是把 customer1 中 address 属性的引用地址复制给 customer2,所以 customer1 和 customer2 中 address 属性指向的是同一个 Address 对象,所以 customer1.getAddress() == customer2.getAddress() 结果为 true。同样的道理,customer1修改了 address 属性值,因为 customer1 和 customer2 指向同一个对象,所以在 customer2 中 address 属性值也会发生变化。
深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
@Data
public class Customer implements Cloneable, Serializable{
private Integer age;
private String name;
private Address address;
public Customer clone() {
Customer customer = null;
try {
customer = (Customer) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return customer;
}
//使用流,实现深克隆
public Customer deepClone() {
try {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Customer) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
@Data
public class Address implements Serializable {
private String name;
}
@Test
public void testCustomerDeepClone() {
Address address = new Address();
address.setName("ChengDu");
Customer customer1 = new Customer();
customer1.setAge(23);
customer1.setName("Tom");
customer1.setAddress(address);
//深克隆
Customer customer2 = customer1.deepClone();
customer1.getAddress().setName("ShangHai");
System.out.println(customer1 == customer2); //false
System.out.println(customer1.getAddress() == customer2.getAddress()); //false
System.out.println(customer1.getAddress().getName()); //ShangHai
System.out.println(customer2.getAddress().getName()); //ChengDu
}
上面代码中实现了深克隆,克隆时会把引用类型 address 复制一份,这样 customer2 和 customer1 中 address 是两个不同的对象。
总结
原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用。
1. 主要优点:
- 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
- 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
- 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
2. 主要缺点:
- 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进
行改造时,需要修改源代码,违背了“开闭原则”。 - 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
3. 适用场景:
-
创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
-
如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
-
需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。