一、概念
1、定义:原型实例指定创建对象的种类,可以通过拷贝这些原型创建新的对象。(不需要知道任何创建的细节,且不调用构造函数)
2、类型:创建型
3、适用场景
- 类初始化消耗较多资源时
- new一个对象的过程繁琐(需要数据准备、访问权限)
- 或构造函数复杂
- 循环体产生大量对象时
4、优缺点
-
优点:性能比直接new一个对象性能高,简化了创建过程
-
缺点:
- 必须配备克隆方法;
- 克隆复杂对象,或是对克隆出的对象进行复杂改造时容易引入风险;
- 深拷贝、浅拷贝要使用得当
5、扩展:
-
深克隆:对于引用类型,如果需要它们指向不同的对象,那么要使用深克隆,深克隆对于某一个类象的引用类型的时候,要显式的写对哪个属性去深克隆
-
浅克隆:默认实现
创建对象是一定要注意是否和预期一致,也就是这个对象是新创建出来的,还是只是创建一个引用,指向的是同一个地址。
6、UML图
二、Coding
业务场景:网站有活动,如积分抽奖等,会发送邮件通知用户,假设邮件对象创建繁琐。
1、设计不好的情况如下:
//较差的设计
//邮件类
public class Mail {
private String name;
private String emailAddress;
private String content;
//后面看克隆时候有没有调用构造器
public Mail() {
System.out.println("Mail的构造器");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "Mail{" +
"name='" + name + '\'' +
", emailAddress='" + emailAddress + '\'' +
", content='" + content + '\'' +
'}';
}
}
//工具类
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());
}
}
//测试类,我们希望
public class Test {
public static void main(String[] args) {
//假设new一个mail很消耗性能
Mail mail = new Mail();
mail.setContent("初始化模版");
for (int i = 0; i < 10; i++) {
//由于记录只能放到最后,只能在这new,每次都要new一个,消耗资源太大
mail = new Mail();
mail.setName("姓名" + i);
mail.setEmailAddress("姓名" + i + "@qq.com");
mail.setContent("恭喜您,此次活动中奖了");
MailUtil.sendMail(mail);
}
//假设实际业务很复杂,存储一定要放在最后,还要记录发送结果等,都要记录到原始的mail对象中,原始的!
MailUtil.saveOriginMailRecord(mail);
}
}
2、使用克隆
原型模式是在内存中进行二进制流的拷贝,肯定要比直接new一个对象性能好。
记住克隆时候式没有调用构造器的,且每次克隆后地址也不一样,
//MailUtil不变
//实现Cloneable接口并重写clone方法
public class Mail implements Cloneable{
private String name;
private String emailAddress;
private String content;
public Mail() {
System.out.println("Mail的构造器");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
//重写克隆方法,调用父类的clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
System.out.println("clone mail object");
return super.clone();
}
@Override
public String toString() {
return "Mail{" +
"name='" + name + '\'' +
", emailAddress='" + emailAddress + '\'' +
", content='" + content + '\'' +
'}';
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
//假设new一个mail很消耗性能
Mail mail = new Mail();
mail.setContent("初始化模版");
for (int i = 0; i < 10; i++) {
//用原始的mail去克隆,让克隆出的tempMail去set,最后保存还是保存原始的mail
Mail tempMail = (Mail) mail.clone();
tempMail.setName("姓名" + i);
tempMail.setEmailAddress("姓名" + i + "@qq.com");
tempMail.setContent("恭喜您,此次活动中奖了");
MailUtil.sendMail(tempMail);
}
//假设实际业务很复杂,存储一定要放在最后
MailUtil.saveOriginMailRecord(mail);
}
}
//只调用了一次Mail构造器
Mail的构造器
clone mail object
向姓名0同学,邮件地址:姓名0@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名1同学,邮件地址:姓名1@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名2同学,邮件地址:姓名2@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名3同学,邮件地址:姓名3@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名4同学,邮件地址:姓名4@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名5同学,邮件地址:姓名5@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名6同学,邮件地址:姓名6@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名7同学,邮件地址:姓名7@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名8同学,邮件地址:姓名8@qq.com,邮件内容:(2)发送邮件成功
clone mail object
向姓名9同学,邮件地址:姓名9@qq.com,邮件内容:(2)发送邮件成功
存储originMail记录,originMail:初始化模版
3、深克隆和浅克隆
3.1浅克隆
public class Pig implements Cloneable{
private String name;
private Date birthday;
public Pig(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
}
public Pig(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Pig{" +
"name='" + name + '\'' +
", birthday=" + birthday +
'}' + super.toString();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Date birthday = new Date(0L);
Pig pig1 = new Pig("佩奇", birthday);
Pig pig2 = (Pig) pig1.clone();
//发现是两个不同的对象,是因为没有重写hashCode
System.out.println(pig1); //Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}Pig@5e2de80c
System.out.println(pig2); //Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}Pig@1d44bcfa
//重新设置pig1的生日后,我们发现pig2的时间也变了,他们引用的都是同一个Date对象,默认是浅克隆
pig1.getBirthday().setTime(6666666666666L);
System.out.println(pig1); //Pig{name='佩奇', birthday=Wed Apr 04 19:51:06 CST 2181}Pig@5e2de80c
System.out.println(pig2); //Pig{name='佩奇', birthday=Wed Apr 04 19:51:06 CST 2181}Pig@1d44bcfa
}
}
3.2深克隆
上边例子是因为生日对象的引用类型没有重写clone方法出现的,使得只是向日期对象创建了一个引用(pig1和pig2中的生日属性都指向同一个日期对象),我们只要在克隆方法中改变一下就行。
//浅拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
//深拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
Pig pig = (Pig) super.clone();
//深克隆,单独为生日实现克隆方法,这样只会改变pig1的生日而不会改变克隆对象的生日
pig.birthday = (Date) pig.birthday.clone();
return pig;
}
4、克隆破坏单例模式
//实现了Cloneable接口,重写了clone方法,直接看最下面
public class HungrySingleton implements Serializable,Cloneable {
private static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
//避免反射攻击
public HungrySingleton() {
if (hungrySingleton != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
//解决序列化和反序列化破坏单例模式
private Object readResolve() {
return hungrySingleton;
}
//克隆破坏单例,这边权限是protected
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
//反射,获取方法名称
Method method = hungrySingleton.getClass().getDeclaredMethod("clone");
method.setAccessible(true); //把protect权限打开
HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton);
System.out.println(hungrySingleton); //HungrySingleton@610455d6
System.out.println(cloneHungrySingleton); //HungrySingleton@511d50c0
}
}
我们发现得到的对象不一样,克隆破坏了单例模式!怎么解决呢?
- 要么单例类不去实现Cloneable接口
- 如果实现了,必须在clone方法中改写成
return getInstance();
public static HungrySingleton getInstance() {
return hungrySingleton;
}
//克隆破坏单例,这边权限是protected
@Override
protected Object clone() throws CloneNotSupportedException {
// 这么写会破坏单例模式
// return super.clone();
//解决方法
return getInstance();
}
三、源码解析
ArrayList中
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);
}
}
通过Arrays.copyOf()方法把元素复制一份
同理,HashMap中
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)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;
}