1. 什么是
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
1.1 对象的创建成本比较大的场景
创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。
但如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。
1.2 最快速地clone一个HashMap散列表
以如何最快速地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,过程会比较耗时。
此时原型模式便可解决该问题。
2. 实现
原型模式基于拷贝已有对象的数据(深拷贝和浅拷贝)实现。
浅拷贝和深拷贝的区别在于:
- 浅拷贝只会复制索引(散列表),不会复制数据(SearchWord 对象)本身。
- 深拷贝不仅仅会复制索引,还会复制数据本身。故深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
2.1 浅拷贝实现
在 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; }}2.2 深拷贝实现
深拷贝实现有两种方案:递归拷贝和对象序列化
2.2.1 递归拷贝
第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。根据这个思路对之前的代码进行重构。
/** * 原型模式--深拷贝-递归拷贝对象 * * 递归拷贝对象、对象的引用对象以及引用对象的引用对象…… * 直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。 * 根据这个思路对之前的代码进行重构 */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; }}2.2.2 对象序列化与反序列化
第二种方法:先将对象序列化,然后再反序列化成新的对象
/** * 原型模式--深拷贝-对象序列化 * * 仅用于提供思路参考,不保证代码本身正确性以及正常运行。 */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; }}2.3 深浅拷贝结合
无论单纯使用深拷贝的哪种实现方式,深拷贝都要比浅拷贝耗时、耗内存空间。
为解决该问题,可以先采用浅拷贝的方式创建 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; }}3. 优缺点
该部分来自《Head First设计模式》,有的地方可能过于抽象或官方语言,仅作相关参考。
3.1 优点
- 向客户隐藏制造新实例的复杂性。
- 提供让客户能够产生未知类型对象的选项。
- 在某些环境下,复制对象比创建对象更有效。
3.2 用途和缺点
- 在一个复杂的类层次中,当系统必须从其中的许多类型创建新对象时(即,当创建给定类的实例的过程很昂贵或者很复杂时),可以考虑原型模式。
- 使用原型模式的缺点:对象的复制有时相当复杂。
参考资料
- 设计模式之美
- 《Head First 设计模式》

评论