原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。
何时使用: 1、当一个系统应该独立于它的产品创建,构成和表示时。一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
关键代码: 1、实现克隆操作,在 JAVA 实现 Cloneable 接口,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。
优点: 1、性能提高。 2、逃避构造函数的约束。
缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。
注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
1.浅克隆
实现原型模式需要实现标记型接口Cloneable,一般会重写clone()方法,如果只是重写clone()方法,而没实现Cloneable接口,调用时会报异常 java.lang.CloneNotSupportedException
例:当执行 p1.loc.street = "sh"; 时 ,p2.loc的street也会改变为 "sh"
原因 : clone() 会把 p1 整个在内存中copy出一份,p1.loc 和p2.loc的指向地址是相同的,改变了一个两个都会改变
public class Test {
public static void main(String[] args) throws Exception {
Person p1 = new Person();
Person p2 = (Person)p1.clone();
System.out.println(p2.age + " " + p2.score);
System.out.println(p2.loc);
System.out.println(p1.loc == p2.loc);
p1.loc.street = "sh";
System.out.println(p2.loc);
}
}
class Person implements Cloneable {
int age = 8;
int score = 100;
Location loc = new Location("bj", 22);
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Location {
String street;
int roomNo;
@Override
public String toString() {
return "Location{" +
"street='" + street + '\'' +
", roomNo=" + roomNo +
'}';
}
public Location(String street, int roomNo) {
this.street = street;
this.roomNo = roomNo;
}
}
2.深克隆
例:loc也需要实现 Cloneable
原因 : clone()时,loc1 也会在内存中clone()一份loc2,两个的地址不同,改变其中一个不会对另一个造成影响
注意: 如果不想clone的对象引用地址相同,则对象也需要内部需要继承Cloneable,并且重写clone方法
public class Test1 {
public static void main(String[] args) throws Exception {
Person1 p1 = new Person1();
Person1 p2 = (Person1)p1.clone();
System.out.println(p2.age + " " + p2.score);
System.out.println(p2.loc);
System.out.println(p1.loc == p2.loc);
p1.loc.street = "sh";
System.out.println(p2.loc);
}
}
class Person1 implements Cloneable {
int age = 8;
int score = 100;
Location1 loc = new Location1("bj", 22);
@Override
public Object clone() throws CloneNotSupportedException {
Person1 p = (Person1)super.clone();
p.loc = (Location1)loc.clone();
return p;
}
}
class Location1 implements Cloneable {
String street;
int roomNo;
@Override
public String toString() {
return "Location{" +
"street='" + street + '\'' +
", roomNo=" + roomNo +
'}';
}
public Location1(String street, int roomNo) {
this.street = street;
this.roomNo = roomNo;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
3.深拷贝——序列化
如果使用重写clone()方法实现深拷贝,那么要将类中所有自定义引用变量的类也去实现Cloneable接口实现clone()方法,这样做就太麻烦了所以我们使用序列化
序列化后将二进制字节流内容写到一个媒介(文本或字节数组),然后是从这个媒介读取数据,原对象写入这个媒介后拷贝给clone对象,原对象的修改不会影响clone对象,因为clone对象是从这个媒介读取。
熟悉对象缓存的知道我们经常将Java对象缓存到Redis中,然后还可能从Redis中读取生成Java对象,这就用到序列化和反序列化。一般可以将Java对象存储为字节流或者json串然后反序列化成Java对象。因为序列化会储存对象的属性但是不会也无法存储对象在内存中地址相关信息。所以在反序列化成Java对象时候会重新创建所有的引用对象。
在具体实现上,所有自定义的类需要实现Serializable接口
在这里将Person2和Location2实现Serializable接口,并且在Person2中实添加deepClone()方法
class Person2 implements Serializable{
int age = 8;
int score = 100;
Location2 loc = new Location2("bj", 22);
public Object deepClone() throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
oos.close();
// 反序列化: 分配内存, 写入原始对象, 生成新对象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Object object = ois.readObject();
return object;
}
}
//调用方式
Person2 p2 = (Person2)p1.deepClone();
或者封装成方法
class CloneUtils {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T obj) {
T cloneObj = null;
try {
//写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
//调用方法
Person2 p2 = CloneUtils.deepClone(p1);
问题:
1.String需要 进一步深克隆吗?
不需要,因为 String指向的是常量池,本来就是共用的,修改其中一个不会修改常量池中的数据,而是哪个变了则自动更改引用地址。
由本身的final性, 每次赋值都是一个新的引用地址,原对象的引用和副本的引用互不影响。
因此String就和基本数据类型一样,表现出了"深拷贝"特性.
2.new String()呢?
需要,因为 new对象实在堆中创建,虽然此对象引用依然指向常量池,但是clone出来的new String 修改是修改的堆中数据,引用改了,元数据的引用跟着就改变了