代码编织梦想

原型模式在java中用到的不多,主要在javascript语言中比较常用

what 是原型模式

如果一个对象创建的成本比较大,而同一个类的不同对象差别不大(大部分字段相同),这个时候,我们可以利用已经有的对象进行复制(clone)或者说拷贝一份,来创建新的对象,达到节省时间的目的 ,简单的说,这种利用已经有的对象来创建新的对象的方法就叫做原型模式

那么我们接着追问,为什么创建对象的成本比较大,或者那种情况下创建对象的成本比较大,拆开来看

  • 申请内存、给成员变量赋值 (本身不花费多少时间,或者相对于业务可以忽略)
  • 包含复杂计算 。排序、计算哈希值(耗时)
  • 耗时io操作,rpc调用、文件读取、数据库等 (耗时)

where 哪里使用

这里举一个demo,假设我们的需求是
数据库中存储了大约 10 万条“搜索关键词”信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动的时候会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词对应的信息,我们给关键词建立一个散列表索引。
我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,我们对 v2 版本的数据进行更新,得到 v3 版本的数据。这里我们假设只有更新和新添关键词,没有删除关键词的行为。
image.png

v2v3
关键词次数更新时间戳关键词次数更新时间戳
算法41123算法41123
算法151234算法1502134
算法261128算法2602136
算法342133

为了保证系统 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列表,
    并且它们互不影响。

image.png

image.png
在 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;
  }
}

进一步思考

一、如果不仅往数据库中添加和更新关键词,还删除关键词,这种情况下,又该如何实现呢?

  1. 考虑到删除关键词,那么最好数据库使用软删除,这样可以知道哪些关键词是被删除的,那么拿到这些被删除的关键词就可以在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,并为它设置了nameage两个属性。接着,我们创建了一个目标对象target,并调用了BeanUtils.copyProperties(source, target)方法,将源对象source的属性值拷贝到目标对象target中。最后,我们验证了目标对象target的属性值是否与源对象source的属性值相同。
需要注意的是,BeanUtils.copyProperties拷贝对象属性时,要求源对象和目标对象的属性名和数据类型必须相同。如果有不同的属性名、数据类型,或者目标对象缺少某些属性,那么拷贝可能失败,或者只能拷贝部分属性值。

它的缺点是
虽然BeanUtils.copyProperties是一个很方便的工具,但它并不完美,存在一些缺点:

  1. 映射不完整:如果源对象和目标对象属性名不一致,或者类型不同,BeanUtils.copyProperties无法进行属性的拷贝。这种情况下,需要使用更加复杂的映射库,如MapStruct、ModelMapper等。
  2. 性能问题:由于BeanUtils.copyProperties采用反射的方式进行属性拷贝,因此相比于手写的拷贝代码,速度较慢。当需要进行大量的对象拷贝操作时,它可能会影响应用的响应时间和性能。在这种情况下,可以考虑使用代码生成工具,如Lombok、AutoValue等,生成高效的拷贝代码。
  3. 无法拷贝私有属性:由于Java的访问权限机制,BeanUtils.copyProperties无法拷贝源对象中的私有属性。如果需要拷贝私有属性,可以使用其他反射库,如Spring的ReflectionUtils、Apache的FieldUtils等。
  4. 不支持拷贝嵌套属性:BeanUtils.copyProperties无法拷贝源对象中的嵌套属性,例如一个Person对象中包含一个Address对象,只能拷贝Person对象中的属性,而无法拷贝Address对象中的属性。如果需要拷贝嵌套属性,可以使用其他工具库,如Dozer、Orika等。

综上所述,虽然BeanUtils.copyProperties是一个方便的工具,但在实际使用过程中需要注意它的局限性,并根据具体情况选择合适的拷贝方式。

案例一

不使用克隆方式,可以使用序列化和反序列化来实现深复制。具体步骤如下:

1. 定义一个实现了 Serializable 接口的类,以支持序列化和反序列化。
2. 使用 ObjectOutputStream 将对象序列化为字节数组。
3. 使用 ByteArrayInputStreamObjectInputStream 将字节数组反序列化为一个新的对象。

下面是一个支持泛型和 ListJava 深复制代码示例:

```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());
      
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_25385555/article/details/129682210

23种设计模式 --原型模式-爱代码爱编程

文章目录 原型模式浅拷贝具体原型类访问类浅克隆存在的问题深拷贝重写clone()方法序列化对象原型模式的优缺点 原型模式 原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它

java设计模式之原型模式_不会敲代码阿的博客-爱代码爱编程

一:原型模式 1.什么是原型模式? 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。 基本介绍 1.原型模式(Prototype模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原

备忘录模式-爱代码爱编程

备忘录模式(Memento Pattern),也叫快照(Snapshot)模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态;这样以后就可将该对象恢复到原先保存的状态。主要是用来防丢失、撤销、恢