如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。
但如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
以如何最快速地clone一个HashMap散列表为例。
当需要将数据库存储的搜索关键字信息存入内存以备后续需求调用。可以直接使用Java语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。我们只需要将数据从数据库中读取出来,放入 HashMap 就可以了。
当另一个系统B同时需要操作该关键字信息数据时,为了保证系统 A 中数据的实时性,只需要在系统 A 中,记录当前数据的版本 Va 对应的更新时间 Ta,从数据库中捞出更新时间大于 Ta 的所有搜索关键词,也就是找出 Va 版本与最新版本数据的“差集”,然后针对差集中的每个关键词进行处理。存在则更新,不存在就插入。
当存在更新的需求,如1、 任何时刻,系统 A 中的所有数据都必须是同一个版本的;2、同时在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据时,解决方案如下:把正在使用的数据的版本定义为“服务版本”,当要更新内存中的数据的时候,并不直接在服务版本(假设是版本 a 数据)上更新,而是重新创建另一个版本数据(假设是版本 b 数据),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。
但这样做新版本(newKeywords )的构建的成本比较高:从数据库中读出,然后计算哈希值,构建 newKeywords,过程会比较耗时。
此时原型模式便可解决该问题。
原型模式基于拷贝已有对象的数据(深拷贝和浅拷贝)实现。
浅拷贝和深拷贝的区别在于:
在 Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。
/**
* 原型模式--浅拷贝
*
* 利用Java 中的 clone() 语法来复制一个对象。
*
* 最耗时的还是从数据库中取数据的操作。相对于数据库的 IO 操作来说,内存操作和 CPU 计算的耗时都是可以忽略的。
*
* 此时处于浅拷贝,当我们通过 newKeywords 更新 SearchWord 对象的时候,newKeywords 和 currentKeywords 因为指向相同的一组 SearchWord 对象,
* 就会导致 currentKeywords 中指向的 SearchWord,有的是老版本的,有的是新版本的
* 从而无法满足:currentKeywords 中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态
*
* 解决方案是改用深拷贝实现。
*/
public class PrototypeDemo3 {
private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
private long lastUpateTime = -1;
public void refresh() {
// 原型模式 拷贝已有对象的数据,更新少量差值
HashMap<String, SearchWord> newKeyWords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
long maxNewUpdateTime = lastUpateTime;
for (SearchWord searchWord: toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
maxNewUpdateTime = searchWord.getLastUpdateTime();
}
if (newKeyWords.containsKey(searchWord.getKeyword())) {
// 存在直接更新
SearchWord oldSearchWOrd = newKeyWords.get(searchWord.getKeyword());
oldSearchWOrd.setCount(searchWord.getCount());
oldSearchWOrd.setLastUpdateTime(searchWord.getLastUpdateTime());
} else {
// 不存在就加入
newKeyWords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpateTime = maxNewUpdateTime;
currentKeywords = newKeyWords;
}
private List<SearchWord> getSearchWords(long lastUpateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
深拷贝实现有两种方案:递归拷贝和对象序列化
第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。根据这个思路对之前的代码进行重构。
/**
* 原型模式--深拷贝-递归拷贝对象
*
* 递归拷贝对象、对象的引用对象以及引用对象的引用对象……
* 直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
* 根据这个思路对之前的代码进行重构
*/
public class PrototypeDemo4 {
private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
private long lastUpateTime = -1;
public void refresh() {
HashMap<String, SearchWord> newKeyWords = new HashMap<>();
for (HashMap.Entry<String, SearchWord> e: currentKeywords.entrySet()) {
SearchWord searchWord = e.getValue();
SearchWord newSearchWord = new SearchWord(searchWord.getKeyword(),
searchWord.getCount(), searchWord.getLastUpdateTime());
newKeyWords.put(e.getKey(), newSearchWord);
}
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
long maxNewUpdateTime = lastUpateTime;
for (SearchWord searchWord: toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
maxNewUpdateTime = searchWord.getLastUpdateTime();
}
if (newKeyWords.containsKey(searchWord.getKeyword())) {
SearchWord oldSearchWOrd = newKeyWords.get(searchWord.getKeyword());
oldSearchWOrd.setCount(searchWord.getCount());
oldSearchWOrd.setLastUpdateTime(searchWord.getLastUpdateTime());
} else {
newKeyWords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpateTime = maxNewUpdateTime;
currentKeywords = newKeyWords;
}
private List<SearchWord> getSearchWords(long lastUpateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
第二种方法:先将对象序列化,然后再反序列化成新的对象
/**
* 原型模式--深拷贝-对象序列化
*
* 仅用于提供思路参考,不保证代码本身正确性以及正常运行。
*/
public class PrototypeDemo5 {
private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
private long lastUpateTime = -1;
public void refresh() {
HashMap<String, SearchWord> newKeyWords = new HashMap<>();
for (HashMap.Entry<String, SearchWord> e: currentKeywords.entrySet()) {
SearchWord searchWord = e.getValue();
SearchWord newSearchWord = new SearchWord(searchWord.getKeyword(),
searchWord.getCount(), searchWord.getLastUpdateTime());
newKeyWords.put(e.getKey(), newSearchWord);
}
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
long maxNewUpdateTime = lastUpateTime;
for (SearchWord searchWord: toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
maxNewUpdateTime = searchWord.getLastUpdateTime();
}
if (newKeyWords.containsKey(searchWord.getKeyword())) {
SearchWord oldSearchWOrd = null;
try {
oldSearchWOrd = (SearchWord) deepCopy(newKeyWords.get(searchWord.getKeyword()));
oldSearchWOrd.setCount(searchWord.getCount());
oldSearchWOrd.setLastUpdateTime(searchWord.getLastUpdateTime());
newKeyWords.replace(oldSearchWOrd.getKeyword(), oldSearchWOrd);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
newKeyWords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpateTime = maxNewUpdateTime;
currentKeywords = newKeyWords;
}
/**
* 先将对象序列化,然后再反序列化成新的对象
* @param object
* @return
* @throws IOException
* @throws ClassNotFoundException
*/
public Object deepCopy(Object object) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
private List<SearchWord> getSearchWords(long lastUpateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
无论单纯使用深拷贝的哪种实现方式,深拷贝都要比浅拷贝耗时、耗内存空间。
为解决该问题,可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象
需要更新的数据是很少的。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的中数据都是老版本的数据。
/**
* 原型模式--深浅拷贝结合
*/
public class PrototypeDemo6 {
private HashMap<String, SearchWord> currentKeywords = new HashMap<String, SearchWord>();
private long lastUpateTime = -1;
public void refresh() {
// 浅拷贝获取所有对象
HashMap<String, SearchWord> newKeyWords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeyWords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpateTime);
long maxNewUpdateTime = lastUpateTime;
for (SearchWord searchWord: toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdateTime) {
maxNewUpdateTime = searchWord.getLastUpdateTime();
}
if (newKeyWords.containsKey(searchWord.getKeyword())) {
newKeyWords.remove(searchWord.getKeyword());
}
newKeyWords.put(searchWord.getKeyword(), searchWord);
}
lastUpateTime = maxNewUpdateTime;
currentKeywords = newKeyWords;
}
private List<SearchWord> getSearchWords(long lastUpateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
该部分来自《Head First设计模式》,有的地方可能过于抽象或官方语言,仅作相关参考。