本文的主要内容如下:
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。
原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。
需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。
原型模式的核心在于如何实现克隆方法。
学过Java语言的人都知道,所有的Java类都继承自 java.lang.Object
。事实上,Object
类提供一个 clone()
方法,可以将一个Java对象复制一份。因此在Java中可以直接使用 Object
提供的 clone()
方法来实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个 标识接口 Cloneable
,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个 CloneNotSupportedException
异常。
public class Mail implements Cloneable{
private String name;
private String emailAddress;
private String content;
public Mail(){
System.out.println("Mail Class Constructor");
}
// ...省略 getter、setter
@Override
protected Object clone() throws CloneNotSupportedException {
System.out.println("clone mail object");
return super.clone();
}
}
在客户端创建原型对象和克隆对象也很简单,如下代码所示:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Mail mail = new Mail();
mail.setContent("初始化模板");
System.out.println("初始化mail:"+mail);
for(int i = 0;i < 3;i++){
System.out.println();
Mail mailTemp = (Mail) mail.clone();
mailTemp.setName("姓名"+i);
mailTemp.setEmailAddress("姓名"+i+"@test.com");
mailTemp.setContent("恭喜您,此次抽奖活动中奖了");
MailUtil.sendMail(mailTemp);
System.out.println("克隆的mailTemp:"+mailTemp);
}
MailUtil.saveOriginMailRecord(mail);
}
}
其中的 MailUtil
工具类为
public class MailUtil {
public static void sendMail(Mail mail) {
String outputContent = "向{0}同学,邮件地址:{1},邮件内容:{2}发送邮件成功";
System.out.println(MessageFormat.format(outputContent, mail.getName(), mail.getEmailAddress(), mail.getContent()));
}
public static void saveOriginMailRecord(Mail mail) {
System.out.println("存储originMail记录,originMail:" + mail.getContent());
}
}
输出如下:
Mail Class Constructor
初始化mail:Mail{name='null', emailAddress='null', content='初始化模板'}com.designpattern.prototype.Mail@12edcd21
clone mail object
向姓名0同学,邮件地址:姓名0@test.com,邮件内容:恭喜您,此次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名0', emailAddress='姓名[email protected]', content='恭喜您,此次抽奖活动中奖了'}com.designpattern.prototype.Mail@34c45dca
clone mail object
向姓名1同学,邮件地址:姓名1@test.com,邮件内容:恭喜您,此次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名1', emailAddress='姓名[email protected]', content='恭喜您,此次抽奖活动中奖了'}com.designpattern.prototype.Mail@52cc8049
clone mail object
向姓名2同学,邮件地址:姓名2@test.com,邮件内容:恭喜您,此次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名2', emailAddress='姓名[email protected]', content='恭喜您,此次抽奖活动中奖了'}com.designpattern.prototype.Mail@5b6f7412
存储originMail记录,originMail:初始化模板
从输出结果中我们可以观察到:
super.clone()
即可。MailUtil.saveOriginMailRecord(mail);
中的 mail
对象的内容仍为 for 循环之前设置的内容,并没有因为克隆而改变。clone
方法,并没有调用 Mail
类的构造器,只在最前面 new
的时候才调用了一次关于输出的内存地址是怎么输出的,我们还需要看一下 Object#toString
方法
public class Object {
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//...省略...
}
所以所谓的内存地址即为 hashCode()
的十六进制表示,这里简单的认为 内存地址相同则为同一个对象,不同则为不同对象
再来看一眼 Object#clone
方法
protected native Object clone() throws CloneNotSupportedException;
这是一个 native
关键字修饰的方法
一般而言,Java语言中的clone()方法满足:
x.clone() != x
,即克隆对象与原型对象不是同一个对象;x.clone().getClass() == x.getClass()
,即克隆对象与原型对象的类型一样;equals()
方法定义恰当,那么 x.clone().equals(x)
应该成立。为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
clone()
方法,并声明为public;clone()
方法中,调用 super.clone()
;此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。
看下面的示例
public class Pig implements Cloneable{
private String name;
private Date birthday;
// ...getter, setter, construct
@Override
protected Object clone() throws CloneNotSupportedException {
Pig pig = (Pig)super.clone();
return pig;
}
@Override
public String toString() {
return "Pig{" +
"name='" + name + '\'' +
", birthday=" + birthday +
'}'+super.toString();
}
}
测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Date birthday = new Date(0L);
Pig pig1 = new Pig("佩奇",birthday);
Pig pig2 = (Pig) pig1.clone();
System.out.println(pig1);
System.out.println(pig2);
pig1.getBirthday().setTime(666666666666L);
System.out.println(pig1);
System.out.println(pig2);
}
}
输出如下
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@312b1dae
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@312b1dae
我们照着上一小节说的实现 Cloneable
,调用 super.clone();
进行克隆,中间我们对 pig1
对象设置了一个时间戳,从输出中我们可以发现什么问题呢?
我们可以发现:
pig1
与 pig2
的内存地址不同pig1
设置了时间,同事 pig2
的时间也改变了我们通过 debug 来看一下
发现如下:
这里引出浅拷贝与深拷贝。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。
浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。
浅克隆:
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。
深克隆:
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
方式一,手动对引用对象进行克隆:
@Override
protected Object clone() throws CloneNotSupportedException {
Pig pig = (Pig)super.clone();
//深克隆
pig.birthday = (Date) pig.birthday.clone();
return pig;
}
方式二,通过序列化的方式:
public class Pig implements Serializable {
private String name;
private Date birthday;
// ...省略 getter, setter等
protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
//将对象写入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ois.readObject());
}
}
饿汉式单例模式如下:
public class HungrySingleton implements Serializable, Cloneable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
if (hungrySingleton != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
private Object readResolve() {
return hungrySingleton;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
使用反射获取对象,测试如下
public class Test {
public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
Method method = hungrySingleton.getClass().getDeclaredMethod("clone");
method.setAccessible(true);
HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton);
System.out.println(hungrySingleton);
System.out.println(cloneHungrySingleton);
}
}
输出
com.designpattern.HungrySingleton@34c45dca
com.designpattern.HungrySingleton@52cc8049
可以看到,通过原型模式,我们把单例模式给破坏了,现在有两个对象了
为了防止单例模式被破坏,我们可以:不实现 Cloneable
接口;或者把 clone
方法改为如下
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
Object
类中的 clone
接口Cloneable
接口的实现类,可以看到至少一千多个,找几个例子譬如:ArrayList
对 clone
的重写如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public Object clone() {
try {
ArrayList> v = (ArrayList>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
//...省略
}
调用 super.clone();
之后把 elementData
数据 copy 了一份
同理,我们看看 HashMap
对 clone
方法的重写:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
@Override
public Object clone() {
HashMap result;
try {
result = (HashMap)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
// ...省略...
}
mybatis 中的 org.apache.ibatis.cache.CacheKey
对 clone
方法的重写:
public class CacheKey implements Cloneable, Serializable {
private List
这里又要注意,updateList
是 List
类型,所以可能是值类型的List,也可能是引用类型的List,克隆的结果需要注意是否为深克隆或者浅克隆
使用原始模式的时候一定要注意为深克隆还是浅克隆。
原型模式的主要优点如下:
原型模式的主要缺点如下:
适用场景:
参考:
刘伟:设计模式Java版
慕课网java设计模式精讲 Debug 方式+内存分析
设计模式 | 简单工厂模式及典型应用
设计模式 | 工厂方法模式及典型应用
设计模式 | 抽象工厂模式及典型应用
设计模式 | 建造者模式及典型应用
更多内容请访问我的个人博客:http://laijianfeng.org/