参考:《Head First 设计模式》
一、原型模式概述
原型模式允许你通过复制现有的实例来创建新的实例,在Java中,这意味着使用clone方法,或者使用序列化和反序列话来实现,这样在不知道要实例化何种特定类的情况下,可以制造出新的实例。
二、原型模式设计
问题:现在有一只羊,姓名为tom,年龄为1,颜色为白色,请编写程序创建和tom属性完全相同的10只羊?
分析:首先最容易想到的便是暴力法,我直接new出10个sheep对象来,这样可以满足问题需求,但是仔细分析会发现这会有很多问题:
- 在创建新的对象时,总是要先获取原对象的属性,比如说姓名、年龄等等,因为原对象都是一样的,所以每次创建时获取的属性都是一样的,这样导致效率过低,
- 不灵活,当原对象发生变化时,这复制的10个对象又要重新获取发生变化的属性,如果只有这些变化倒还好,但如果是原对象增加或者删除某些属性时,那使用new出来的这些克隆对象必须要重新增加或删除这些属性
改进:很自然的就会想到使用Object类的clone方法,该方法可以将一个Java对象复制一份,但是需要实现clone的Java类必须要实现一个接口Cloneable, 该接口表示该类能够复制且具有复制的能力
Sheep类:
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/2 16:29
*/
public class Sheep implements Cloneable{
private String name;
private int age;
private String color;
public Sheep(String name, int age, String color) {
super();
this.name = name;
this.age = age;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Sheep [name=" + name + ", age=" + age + ", color=" + color + "]";
}
}
Client客户端类:
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/2 20:08
*/
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Sheep sheep = new Sheep("tom", 18, "白色");
Sheep sheep2 = (Sheep) sheep.clone();
Sheep sheep3 = (Sheep) sheep.clone();
Sheep sheep4 = (Sheep) sheep.clone();
Sheep sheep5 = (Sheep) sheep.clone();
//.....
//false
System.out.println(sheep.hashCode() == sheep2.hashCode());
}
}
输出是false说明这些对象都是不同的对象,用这种方式便是原型模式,其实这种拷贝的方式在之前用到的很多。
三、深浅拷贝
说到clone方法,那就必须要谈谈深浅拷贝。上面的案例中clone方法是直接是使用的父类的clone方法,如果是被拷贝的对象中的属性只是String类型或者基本数据类型,那么深浅拷贝的结果应该是一样的;但是如果被拷贝的对象中有引用类型,那么浅拷贝只是拷贝该属性的地址,那么原对象与拷贝后的对象中该引用类型的属性指向的是同一片内存空间。
为什么String类型可以做到深拷贝?
因为String类型的特殊性,每次一该变String类将会开辟新的空间,String虽然属于引用类型,但是String类是不可改变的,它是一个常量,一个对象调用clone方法,克隆出一个新对象,这时候两个对象的同一个String类型的属性是指向同一片内存空间的,但是如果改变了其中一个,会产生一片新的内存空间,此时该对象的这个属性的引用将指向这片新的内存空间,可参考这篇文章
举个例子,上述Sheep对象中添加一个引用属性
private Sheep friend;
然后在客户端(main方法)中添加如下测试:
Sheep friend = new Sheep("ketty",19 ,"黑白相见色");
Sheep sheep1 = new Sheep("tom", 18, "白色");
sheep1.setFriend(friend);
Sheep sheep2 = (Sheep) sheep1.clone();
//true
System.out.println(sheep1.getFriend().hashCode() == sheep2.getFriend().hashCode());
输出是true就已经说明了问题,也就是sheep1和sheep2中的friend属性指向的是同一片内存空间,当sheep1中的该属性发生变化,sheep2中的该属性也随之变化。
要做到深拷贝可以有两种方法:①重写clone方法;②使用序列化机制
1.重写clone方法
这里采用自己写一个deepClone方法
protected Object deepClone() throws CloneNotSupportedException {
Sheep copy = (Sheep)super.clone();
Sheep friend = (Sheep)copy.getFriend().clone();
copy.setFriend(friend);
return copy;
}
分析:首先先调用父类的clone方法来实现将String和基本数据类型进行拷贝,然后在引用类型上需要自己new一个新的该类型,然后传给拷贝后的对象,这种方法有个弊端,如果引用类型比较多的话需要一个一个的进行拷贝,比较繁琐,所以就重点介绍下面的另一种方法。
2.序列化的方式
protected Object deepClone() throws CloneNotSupportedException {
Object copy = null;
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
copy = ois.readObject();
}catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
// TODO: handle exception
System.out.println(e2.getMessage());
}
}
return copy;
}
分析:利用序列化将该对象用过字节数组写入到内存中,然后通过反序列化将该数组从内存中读出来,这样就实现了深拷贝。
使用这种方法的前提是该对象必须实现序列化接口,这种方法比之上面的方法更加方便,当有多个引用类型时。