009原型模式-爱代码爱编程
原型模式在java中用到的不多,主要在javascript语言中比较常用
what 是原型模式
如果一个对象创建的成本比较大,而同一个类的不同对象差别不大(大部分字段相同),这个时候,我们可以利用已经有的对象进行复制(clone)或者说拷贝一份,来创建新的对象,达到节省时间的目的 ,简单的说,这种利用已经有的对象来创建新的对象的方法就叫做原型模式
那么我们接着追问,为什么创建对象的成本比较大,或者那种情况下创建对象的成本比较大,拆开来看
- 申请内存、给成员变量赋值 (本身不花费多少时间,或者相对于业务可以忽略)
- 包含复杂计算 。排序、计算哈希值(耗时)
- 耗时io操作,rpc调用、文件读取、数据库等 (耗时)
where 哪里使用
这里举一个demo,假设我们的需求是
数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动的时候会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息,我们给关键词建立一个散列表索引。
我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,我们对 v2 版本的数据进行更新,得到 v3 版本的数据。这里我们假设只有更新和新添关键词,没有删除关键词的行为。
v2 | v3 | ||||
---|---|---|---|---|---|
关键词 | 次数 | 更新时间戳 | 关键词 | 次数 | 更新时间戳 |
算法 | 4 | 1123 | 算法 | 4 | 1123 |
算法1 | 5 | 1234 | 算法1 | 50 | 2134 |
算法2 | 6 | 1128 | 算法2 | 60 | 2136 |
算法3 | 4 | 2133 |
为了保证系统 A 中数据的实时性(不一定非常实时,但数据也不能太旧),系统 A 需要定期根据数据库中的数据,更新内存中的索引和数据。
怎么实现这个需求呢?
- 在系统a中,记录当前版本的对应的更新时间戳,从数据库中查出大于 ta的所有搜索关键字,找出ta和最新的差集,进行更加既可以了
public class Demo {
private ConcurrentHashMap<String, SearchWord> currentKeywords = new ConcurrentHashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (currentKeywords.containsKey(searchWord.getKeyword())) {
currentKeywords.replace(searchWord.getKeyword(), searchWord);
} else {
currentKeywords.put(searchWord.getKeyword(), searchWord);
}
}
lastUpdateTime = maxNewUpdatedTime;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
那么我们需求变动了呢,
- 要求任何时刻,系统a所有数据都必须是同一个版本的,要么是a,要么是b,
- 更新内存数据的时候,a不能处于不可以状态
一个最简单的方式,就是数据查出所有数据,方式newmap中,然后,oldmap=newmap交换指针
缺点是什么,数据量多的时候,非常耗时、耗费内存
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
public void refresh() {
HashMap<String, SearchWord> newKeywords = new LinkedHashMap<>();
// 从数据库中取出所有的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords();
for (SearchWord searchWord : toBeUpdatedSearchWords) {
newKeywords.put(searchWord.getKeyword(), searchWord);
}
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords() {
// TODO: 从数据库中取出所有的数据
return null;
}
}
那么我们,思考下,这中场景特别时候使用原型模式 ,解决这种创建对象耗费时间问题
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = 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);
}
}
lastUpdateTime = maxNewUpdatedTime;
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
这里我们利用了 Java 中的 clone() 语法来复制一个对象。不在程序计算哈希值,同时解决了最耗时的从数据库中取数据的操作。相对于数据库的 IO 操作来说,内存操作和 CPU 计算的耗时都是可以忽略的。
那么这段代码有问题吗,这里延时出2个关键概念
深拷贝和浅拷贝
class Person implements Cloneable {
private String name;
private int age;
private ArrayList<String> hobbies;
// 深克隆方法
public Person clone() {
Person clone = null;
try {
clone = (Person) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
// 深克隆 hobbies
clone.hobbies = (ArrayList<String>) this.hobbies.clone();
return clone;
}
}
这个示例中,我们定义了一个Person类,并实现了深克隆方法clone()。在Person类中,
有一个hobbies字段,它是列表类型的。在clone()方法中,我们通过super.clone()方法创
建了一个副本,并独立地克隆了hobbies列表,以确保克隆的对象与原始对象拥有自己的hobbies列表,
并且它们互不影响。
在 Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。
那么上面代码中,使用的就是浅拷贝,当我们通过 newKeywords 更新 SearchWord 对象的时候(比如,更新“设计模式”这个搜索关键词的访问次数),newKeywords 和 currentKeywords 因为指向相同的一组 SearchWord 对象,就会导致 currentKeywords 中指向的 SearchWord,有的是老版本的,有的是新版本的,就没法满足我们之前的需求:currentKeywords 中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态。
怎么解决这个问题呢?
很简单,我们替换浅拷贝为深拷贝就可以了
一、递归拷贝对象
对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。根据这个思路对之前的代码进行重构
// Deep copy
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);
}
二、序列化拷贝对象
先将对象序列化,然后再反序列化成新的对象。具体的示例代码如下所示:
public Object deepCopy(Object object) {
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();
}
进一步思考,上面的2种方法,无论是哪一种,深拷贝都比浅拷贝耗时,有没有更加好的方法呢
我们可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们再使用深度拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。毕竟需要更新的数据是很少的。
这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的中数据都是老版本的数据。
public class Demo {
private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
private long lastUpdateTime = -1;
public void refresh() {
// Shallow copy
HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
long maxNewUpdatedTime = lastUpdateTime;
for (SearchWord searchWord : toBeUpdatedSearchWords) {
if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
maxNewUpdatedTime = searchWord.getLastUpdateTime();
}
if (newKeywords.containsKey(searchWord.getKeyword())) {
newKeywords.remove(searchWord.getKeyword());
}
newKeywords.put(searchWord.getKeyword(), searchWord);
}
lastUpdateTime = maxNewUpdatedTime;
currentKeywords = newKeywords;
}
private List<SearchWord> getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据
return null;
}
}
进一步思考
一、如果不仅往数据库中添加和更新关键词,还删除关键词,这种情况下,又该如何实现呢?
- 考虑到删除关键词,那么最好数据库使用软删除,这样可以知道哪些关键词是被删除的,那么拿到这些被删除的关键词就可以在clone出来的newKeywords基础上,直接remove掉已经删除的哪些关键词就可以了。反之如果不是使用的软删除,那么就不好使用原型模式,需要获取新版本全量数据,然后和旧版本数据一一比对,看哪些数据是被删除的了。
二、业务代码中哪里利用了原型模式呢?
java分层架构中各层的对象,比如VO,BO,PO之间的互相转换,使用的就是原型模式,而做业务开发每天都要与这些打交道。最为常用经典就是BeanUtils
BeanUtils.copyProperties是Spring框架中常用的一个对象之间拷贝的方法。
可以将一个JavaBean中的属性值拷贝到另一个JavaBean中对应的属性中,可以用于快速复制对象的属性值,而无需手动逐个复制。例如:
Person source = new Person();
source.setName("Alice");
source.setAge(25);
Person target = new Person();
BeanUtils.copyProperties(source, target);
System.out.println(target.getName()); // 输出 "Alice"
System.out.println(target.getAge()); // 输出 "25"
在这个例子中,我们创建了一个源对象source,并为它设置了name和age两个属性。接着,我们创建了一个目标对象target,并调用了BeanUtils.copyProperties(source, target)方法,将源对象source的属性值拷贝到目标对象target中。最后,我们验证了目标对象target的属性值是否与源对象source的属性值相同。
需要注意的是,BeanUtils.copyProperties拷贝对象属性时,要求源对象和目标对象的属性名和数据类型必须相同。如果有不同的属性名、数据类型,或者目标对象缺少某些属性,那么拷贝可能失败,或者只能拷贝部分属性值。
它的缺点是
虽然BeanUtils.copyProperties是一个很方便的工具,但它并不完美,存在一些缺点:
- 映射不完整:如果源对象和目标对象属性名不一致,或者类型不同,BeanUtils.copyProperties无法进行属性的拷贝。这种情况下,需要使用更加复杂的映射库,如MapStruct、ModelMapper等。
- 性能问题:由于BeanUtils.copyProperties采用反射的方式进行属性拷贝,因此相比于手写的拷贝代码,速度较慢。当需要进行大量的对象拷贝操作时,它可能会影响应用的响应时间和性能。在这种情况下,可以考虑使用代码生成工具,如Lombok、AutoValue等,生成高效的拷贝代码。
- 无法拷贝私有属性:由于Java的访问权限机制,BeanUtils.copyProperties无法拷贝源对象中的私有属性。如果需要拷贝私有属性,可以使用其他反射库,如Spring的ReflectionUtils、Apache的FieldUtils等。
- 不支持拷贝嵌套属性:BeanUtils.copyProperties无法拷贝源对象中的嵌套属性,例如一个Person对象中包含一个Address对象,只能拷贝Person对象中的属性,而无法拷贝Address对象中的属性。如果需要拷贝嵌套属性,可以使用其他工具库,如Dozer、Orika等。
综上所述,虽然BeanUtils.copyProperties是一个方便的工具,但在实际使用过程中需要注意它的局限性,并根据具体情况选择合适的拷贝方式。
案例一
不使用克隆方式,可以使用序列化和反序列化来实现深复制。具体步骤如下:
1. 定义一个实现了 Serializable 接口的类,以支持序列化和反序列化。
2. 使用 ObjectOutputStream 将对象序列化为字节数组。
3. 使用 ByteArrayInputStream 和 ObjectInputStream 将字节数组反序列化为一个新的对象。
下面是一个支持泛型和 List 的 Java 深复制代码示例:
```java
import java.io.*;
import java.util.List;
public class DeepCopyUtil {
@SuppressWarnings("unchecked")
public static <T> T deepCopy(T object) throws IOException, ClassNotFoundException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(object);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
return (T) ois.readObject();
}
public static <T> List<T> deepCopyList(List<T> list) throws IOException, ClassNotFoundException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(list);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
return (List<T>) ois.readObject();
}
}
使用示例:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<String> originalList = new ArrayList<>();
originalList.add("item1");
originalList.add("item2");
originalList.add("item3");
// 使用深复制方法复制List
List<String> copiedList = DeepCopyUtil.deepCopyList(originalList);
// 修改原始List用于比较
originalList.set(0, "newItem1");
// 打印出原始List和复制后的List,以及修改后的原始List
System.out.println("Original List: " + originalList);
System.out.println("Copied List: " + copiedList);
}
}
输出:
Original List: [newItem1, item2, item3]
Copied List: [item1, item2, item3]
这种方式相对于 Cloneable 接口来说,需要手动实现序列化和反序列化接口,但是更加灵活,可以深度复制任何复杂的对象。
```java
不使用克隆方式,可以通过遍历对象的所有属性进行复制,实现深复制。具体步骤如下:
1. 定义一个类,实现 Cloneable 接口。
2. 重写 clone 方法,在其中对每个属性进行复制。
3. 如果属性本身也是可复制的,可以使用该属性的 clone 方法进行复制。
以下是一个实现了深复制泛型和 List 的 Java 代码示例:
```java
import java.util.ArrayList;
import java.util.List;
public class DeepCopyUtil {
public static <T> List<T> deepCopyList(List<T> list) {
List<T> copy = new ArrayList<T>(list.size());
for (T element : list) {
T clonedElement = deepCopy(element);
copy.add(clonedElement);
}
return copy;
}
@SuppressWarnings("unchecked")
public static <T> T deepCopy(T object) {
try {
T copy = (T) object.getClass().newInstance();
for (java.lang.reflect.Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object fieldObject = field.get(object);
if (fieldObject instanceof List) {
List<?> listCopy = deepCopyList((List<?>) fieldObject);
field.set(copy, listCopy);
} else if (fieldObject instanceof Cloneable) {
Cloneable toClone = (Cloneable) fieldObject;
Object clone = toClone.getClass().getMethod("clone", new Class[0]).invoke(toClone, new Object[0]);
field.set(copy, clone);
} else {
field.set(copy, fieldObject);
}
}
return copy;
} catch (Exception e) {
throw new RuntimeException("Failed to clone object", e);
}
}
}
使用示例:
public class Main {
public static void main(String[] args) {
List<String> originalList = new ArrayList<>();
originalList.add("item1");
originalList.add("item2");
originalList.add("item3");
// 使用深复制方法复制List
List<String> copiedList = DeepCopyUtil.deepCopyList(originalList);
// 修改原始List用于比较
originalList.set(0, "newItem1");
// 打印出原始List和复制后的List,以及修改后的原始List
System.out.println("Original List: " + originalList);
System.out.println("Copied List: " + copiedList);
}
}
输出:
Original List: [newItem1, item2, item3]
Copied List: [item1, item2, item3]
这种方式需要对每个属性进行手动的判断和复制,相对于序列化和反序列化方式更加繁琐。
以下是一些可以实现深拷贝的Java类库:
1. Apache Commons BeanUtils:提供了copyProperties()方法来实现对象的深拷贝。
2. Spring Framework:Spring提供了BeanUtils和SerializationUtils来实现对象的深拷贝。
3. Google Guava:Guava提供了ImmutableList,ImmutableMap和ImmutableSet来实现集合对象的深拷贝。
4. Cloneable接口:Cloneable接口可以实现对象的拷贝,但需要注意的是,需要重写Object类中的clone()方法并且实现深拷贝。
5. Serializable接口:通过实现Serializable接口,可以使用Java的序列化机制来实现对象的深拷贝。
需要注意的是,对象的深拷贝需要考虑该对象所包含的属性是否也需要进行深拷贝,不然只进行浅拷贝可能会导致对象共享属性,进而出现不可预知的错误。
## 项目中注意
### 1、 使用 BeanUtils.copyProperties(patent,oldPatent); 导致属性丢失
toDoEditParam是前端传递参数 , patent查询数据库所得,toDoEditParam没有 getCaseNo参数 ,复制导致 patent的getCaseNo为空,后续赋值错误
```java
Long id = toDoEditParam.getId();
Patent patent = this.patentService.checkIsExistsAndGet(id);
Patent oldPatent = new Patent();
BeanUtils.copyProperties(patent,oldPatent);
Integer applyStatus = patent.getApplyStatus();
// 同样字段 导致null覆盖
BeanUtils.copyProperties(toDoEditParam, patent);
// 重新填充,不清楚old代码含义
patent.setCaseNo(oldPatent.getCaseNo());
//再次提交,下一步部门领导审核,企业微信通知下一步处理人
ipmNotifyService.notifyNextProcessor(ApproverTypeEnum.LEADER.getCode(),patent.getCurHandleManCas(),patent.getIpType(),patent.getApplyCaseName()
// 此处 getCaseNo ==null
,patent.getCaseNo(),patent.getProponent(),patent.getApplyStatus());