likes
comments
collection
share

【设计模式】创建型模式其五: 原型模式

作者站长头像
站长
· 阅读数 8

创建型模式其五:《原型模式》

原型模式(复制自身)

【设计模式】创建型模式其五: 原型模式 光从这个名字,就大概能知道这个原型模式的含义, 以自身为原型,复制与自己相似的对象

  • 孙悟空:根据自己的形状复制(克隆)出多个身外身
  • 软件开发:通过复制一个原型对象得到多个与原型对象一模一样的新对象

原型模式:使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

总之,原型模式就是克隆本身

原型模式的定义

  • 工作原理:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程
  • 创建新对象(也称为克隆对象)的工厂就是原型类自身,工厂方法由负责复制原型对象的克隆方法来实现 通过克隆方法所创建的对象是全新的对象它们在内存中拥有新的地址,每一个克隆对象都是独立的

原型模式结构

原型模式包含以下3个角色:

  • Prototype(抽象原型类)
  • ConcretePrototype(具体原型类)
  • Client(客户类)

原型模式究竟有什么用

以下是原型模式的一些用途:

  1. 提高性能:通过复制现有对象来创建新对象,可以避免创建新对象时的初始化操作和其他开销,从而提高性能。这里我解释一下:复制现有对象的属性是直接在内存空间找到那个值,直接引用它的值即可,但是new的话是要重新初始化值,因此会比new操作更快,性能更好
  2. 简化对象创建:当需要创建多个具有相似属性的对象时,使用原型模式可以简化对象创建过程。
  3. 保护对象:原型模式可以通过将对象复制到新实例来保护原始对象的状态。这样,如果新实例被修改或损坏,原始对象仍然保持不变。克隆出来的对象是一个全新的对象,重新分配内存空间的
  4. 支持动态配置对象:原型模式可以通过复制现有对象并根据需要修改其属性来支持动态配置对象。

克隆模式分析

克隆其实是分为浅克隆与深克隆,Java的clone()方法默认是浅克隆,下面我依次讲解。

super.clone();为什么调用super的clone

调用Object的clone()方法

在Java中,Object类中的clone()方法是用于创建并返回当前对象的一个副本。当我们要实现一个可克隆的类时,通常需要在该类中覆盖clone()方法,并在该方法中调用super.clone()方法来创建并返回当前对象的副本。

浅克隆

【设计模式】创建型模式其五: 原型模式

当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制(指复制的是引用类型的地址)

总之,就是复制了值类型的值,引用类型复制它的地址

浅克隆模式实例分析

在使用某OA系统时,有些岗位的员工发现他们每周的工作都大同小异,因此在填写工作周报时很多内容都是重复的。为了提高工作周报的创建效率,大家迫切希望有一种机制能够快速创建相同或者相似的周报,包括创建周报的附件。 试使用原型模式对该OA系统中的工作周报创建模块进行改进。

分析: 周报内容很多都是重复的,所以可以直接复制,哪里不同就修改少部分。当然,想调用clone方法可以先实现Cloneable接口。

也可以直接覆写clone()方法,因为它是Object类的方法,不一定要继承Cloneable接口。

而且Cloneable接口只是一个标记接口,里面没有任何实际代码。

但是显示的继承Cloneable接口,有以下好处:

  • 向编译器和用户表明这个类能被克隆
  • 避免在运行时抛出 CloneNotSupportedException
  • 给 API 的用户一个明确的信号,这个类能被正确地克隆

周报代码(需要被克隆的对象)

实现cloneable接口

public class WeeklyLog implements Cloneable {
    //为了简化设计和实现,假设一份工作周报中只有一个附件对象,实际情况中可以包含多个附件,可以通过List等集合对象来实现
   private Attachment attachment;
   private String name;
   private String date;
   private String content;

    public void setAttachment(Attachment attachment) {
      this.attachment = attachment;
   }

   public void setName(String name) {
      this.name = name;
   }

   public void setDate(String date) {
      this.date = date;
   }

   public void setContent(String content) {
      this.content = content;
   }

   public Attachment getAttachment() {
      return (this.attachment);
   }

   public String getName() {
      return (this.name);
   }

   public String getDate() {
      return (this.date);
   }

   public String getContent() {
      return (this.content);
   }

    //使用clone()方法实现浅克隆
   public WeeklyLog clone() {
      Object obj = null;
      try {
         obj = super.clone();
         return (WeeklyLog)obj;
      }
      catch(CloneNotSupportedException e) {
         System.out.println("不支持复制!");
         return null;
      }
   }
}

附件类代码(引用类型的变量)

public class Attachment {
   private String name; //附件名

   public void setName(String name) {
      this.name = name;
   }
   public String getName() {
      return this.name;
   }
    public void download() {
       System.out.println("下载附件,文件名为" + name);
    }
}

客户端调用

public class Client {
   public static void main(String args[]) {
      WeeklyLog log_previous, log_new;
      log_previous = new WeeklyLog();          //创建原型对象
      Attachment attachment = new Attachment();    //创建附件对象
      log_previous.setAttachment(attachment);       //将附件添加到周报中
      log_previous.setName("猫猫");                 // 原型对象添加属性
      log_new = log_previous.clone();             //调用克隆方法创建克隆对象
      //比较周报
      System.out.println("周报是否相同? " + (log_previous == log_new)); // false
      //比较附件
      System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment())); // true
      // 克隆对象的名字
      System.out.println("克隆对象的名字:" + log_new.getName()); // 猫猫
      System.out.println("名字的地址是否相同?" + (log_new.getName() == log_previous.getName())); // true
      // 克隆对象设置name
      log_new.setName("狗");
      // 按照常理来说,修改引用类型的值应该返回true,但是String类型的变量是不可变的,它会重新分配内存,创建新对象。
      System.out.println("名字的地址是否相同?" + (log_new.getName() == log_previous.getName())); // false
   }
}

通过代码可以看出,当我们调用clone()方法,Java其实是进行了一次新建对象操作,分配了一个新的内存空间,因此第一个输出为false。 而附件之所以相同,是因为复制了它的地址,因此相同,当我们修改附件的值时,原来的对象里的值也会发生变化 有一些特例: 按照常理来说,修改引用类型里的值之后,引用类型的地址应该不会改变,但是String类型的变量是不可变的,它会重新分配内存,创建新对象,所以地址会改变。

在Java中,clone()方法是通过浅拷贝的方式来实现对象的复制。具体来说,当我们调用一个对象的clone()方法时,Java会创建一个新的对象,并将原对象中的所有成员变量的值复制到新对象中。对于基本数据类型成员变量,直接进行值复制;对于引用类型成员变量,复制的是引用的地址,即新对象中的引用类型成员变量和原对象中的相应成员变量指向同一个对象。

通过分析,发现浅会克隆响克隆本体的改变,这是不对的因此,出现了深克隆。

深克隆

深克隆无非就是将引用类型的变量也克隆一份,而不是单纯的克隆地址。

我所知道的克隆方式有两种:

先讲第一种: (在调用最外层对象的克隆方法时,如果它的变量为引用类型变量,就对其调用它自身的克隆方法)

就是反复调用克隆方法,直到克隆到最后只是对值类型的克隆

步骤:

  1. 将对象里的所有引用属性都去实现Cloneable接口, 然后每个clone()方法都进行属性判断,如果是引用类型,则再次进行克隆方法的调用。

像这样:

// 通过反射获得属性
WeeklyLog weeklyLog = (WeeklyLog) super.clone();
Field[] declaredFields = weeklyLog.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
   Class<?> type = declaredField.getType();
   if(type != String.class || type != Integer.class ){ // 没写完,就是去排除基本类型
      // 调用clone方法
   }
}

上面的方法挺麻烦的,不仅要排除基本类型,而且可能会死循环(当它的某个属性为父类实例时)

  1. 用Stream流去读取

WeeklyLog代码改写

这样就必须实现Serializable,而不用实现CLoneAble接口。

public class WeeklyLog implements Serializable {
   private Attachment attachment;
   private String name;
   private String date;
   private String content;

   public void setAttachment(Attachment attachment) {
      this.attachment = attachment;
   }

   public void setName(String name) {
      this.name = name; 
   }

   public void setDate(String date) {
      this.date = date; 
   }

   public void setContent(String content) {
      this.content = content; 
   }

   public Attachment getAttachment() {
      return (this.attachment);
   }

   public String getName() {
      return (this.name); 
   }

   public String getDate() {
      return (this.date); 
   }

   public String getContent() {
      return (this.content); 
   }

   //使用序列化技术实现深克隆
   public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException {
      //将对象写入流中
      ByteArrayOutputStream bao=new ByteArrayOutputStream();
      ObjectOutputStream oos=new ObjectOutputStream(bao);
      oos.writeObject(this);
      
      //将对象从流中取出
      ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());
      ObjectInputStream ois=new ObjectInputStream(bis);
      return (WeeklyLog)ois.readObject();
   }
}

可以使用序列化技术实现深克隆,因为序列化过程中会将对象及其所有引用类型成员变量的状态保存到一个流中,然后反序列化过程中会从流中读取对象及其所有引用类型成员变量的状态,并创建一个新的对象。由于序列化和反序列化过程都是在内存中进行的,因此可以保证新对象和原对象之间不存在共享的引用类型成员变量

具体来说,序列化过程中,对象及其所有引用类型成员变量的状态都会被保存到一个字节流中,然后通过反序列化过程,可以将这个字节流转换为一个新的对象,新对象和原对象之间不存在共享的引用类型成员变量,即实现了深克隆。

需要注意的是,要实现序列化和反序列化,类必须实现Serializable接口,否则会抛出NotSerializableException异常。另外,序列化和反序列化过程中,对象及其成员变量的访问修饰符必须是public或protected,否则会抛出IllegalAccessException异常。

Serializable接口知识点补充

Serializable是一个标记接口,实现Serializable接口的类会被Java序列化机制自动识别,可以将其实例转换为字节流进行传输或者保存到文件中。

这种方法很常用,但也不是万能的。

序列化和反序列化过程本身不会导致栈溢出。但是,在实现深克隆时,如果对象的层次结构比较深或者对象图比较复杂,可能会导致序列化和反序列化过程中创建大量对象,从而导致内存占用过高,最终导致OutOfMemoryError异常

另外,如果需要序列化的对象中包含循环引用,即某个对象的成员变量引用了该对象本身或者其祖先对象,那么在序列化和反序列化过程中可能会出现死循环,导致程序无法正常运行或者栈溢出。

因此,在实现深克隆时,需要注意以下几点:

  1. 对象的层次结构和对象图比较复杂时,需要考虑使用其他方式实现深克隆,如手动递归拷贝每个对象及其成员变量。
  2. 对象中不要包含循环引用。
  3. 在序列化和反序列化过程中,需要考虑内存占用情况,如果需要序列化的对象比较大,可以考虑分块序列化和反序列化,或者使用其他方式实现深克隆。

模式优缺点

模式优点

  • 简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率 扩展性较好
  • 提供了简单的创建结构,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品
  • 可以使用深克隆的方式保存对象的状态,以便在需要的时候使用,可辅助实现撤销操作

模式缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了开闭原则
  • 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦

反射还是很重要的,虽然这个例子用反射还挺麻烦的,但是到后面的配置文件一般都有反射,建议大家去了解了解。

转载自:https://juejin.cn/post/7222462368169017400
评论
请登录