Java设计模式-创建型设计模式-原型模式

Posted FatterShday

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java设计模式-创建型设计模式-原型模式相关的知识,希望对你有一定的参考价值。

Java设计模式-创建型设计模式-原型模式

从这一专栏开始将学习设计模式,上课学习和自己总结归纳的笔记将总结出来供大家参考。
参考书籍:《设计模式就该这样学》

其他文章:

Java设计模式-UML类图Java设计模式-七大架构设计原则-开闭原则
Java设计模式-七大架构设计原则-依赖倒置原则Java设计模式-七大架构设计原则-单一职责原则
Java设计模式-七大架构设计原则-接口隔离原则Java设计模式-七大架构设计原则-最少知道原则(迪米特法则)
Java设计模式-七大架构设计原则-里氏替换原则和合成复用原则Java设计模式-创建型设计模式-简单工厂模式
Java设计模式-创建型设计模式-工厂方法模式(工厂模式)Java设计模式-创建型设计模式-抽象工厂模式
Java设计模式-创建型设计模式-建造者模式Java设计模式-创建型设计模式-原型模式
Java设计模式-创建型设计模式-单例模式Java设计模式-结构型设计模式-适配器模式
Java设计模式- 结构型设计模式-享元模式Java设计模式- 结构型设计模式-外观模式
Java设计模式- 结构型设计模式-桥接模式Java设计模式-结构型模式设计模式-组合模式
Java设计模式-行为型设计模式-观察者模式

文章目录

一、创建型设计模式

创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离,对客户端代码需要调用对象的时候隐藏了类的实例化的创建细节

其中包括:简单工厂模式(不在GoF23种设计模式中)、工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式。

二、原型模式

1.原型模式定义

​ 用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。说大白话就是自己复制自己,通过原生对象复制出一个新的对象,这两个对象结构相同且相似,但不是通过new出来的(不调用构造函数)

需要注意的是,原型对象自己不仅是个对象还是个工厂。并且通过克隆方式创建的对象是全新的对象,它们都是有自己的新的地址,通常对克隆模式所产生的新对象进行修改,是不会对原型对象造成任何影响的,每一个克隆对象都是相对独立的,通过不同的方式对克隆对象进行修改后,可以的到一系列相似但不完全相同的对象。

​ 原型模式我们也称为克隆模式,即一个某个对象为原型克隆出来一个一模一样的对象,该对象的属性和原型对象一模一样。而且对于原型对象没有任何影响。原型模式的克隆方式有两种:浅克隆和深克隆

在GoF23种设计模式中属于创建型设计模式:

​ 其中包括:简单工厂模式(不在GoF23种设计模式中)、工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式。

2.浅克隆和深克隆

浅克隆:复制对象时仅仅复制对象本身,包括基本属性,但该对象的属性引用其他对象时,该引用对象不会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个。

深克隆:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深克隆把要复制的对象所引用的对象都复制了一遍。

3.原型模式的角色:

抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口,甚至还可以是具体实现类。

具体原型类(ConcretePrototype):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

客户类(Client):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

4.原型模式的特点

优点:

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

缺点:

  1. 需要为每一个类准备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有类进行改造时,需要修改原代码,违背了开闭原则
  2. 在实现深克隆时需要写较复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类必须支持深克隆,实现起来较繁琐.

适用环境

  1. 创建新对象成本较大,新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量修改
  2. 系统要保存对象的状态,而对象的状态变化很小
  3. 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更方便

5.原型模式的类图

6.原型模式的代码实现

6.1Java中如何实现对象的克隆

在Java中可以直接使用Object提供的clone方法实现对象的克隆。但需要注意的是,能够实现克隆的java类必须实现一个标识接口:Cloneable,标识这个Java类支持复制。如果一个类没有实现这个接口而调用了clone方法,java编译器将抛出一个CloneNotSupportedException异常。

Clone方法的条件:

1.对于任何对象x,有x.clone()!=x

2.对于任何对象x,有x.clone().getClass()==x.getClass()

3.如果x的equals方法定义恰当,那么x.clone().equals(x)应当成立

6.2具体案例:

类图如下:

6.2.1浅克隆具体代码实现:

1.附件类Attachment

/**
 * 附件类
 */
public class Attachment 
    private String name;

    public String getName() 
        return name;
    

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

    public void download()
        System.out.println("下载附件,文件名为:" + this.name);
    


2.原型类WeeklyLog

/**
 * WeeklyLog原型类
 * @author WxrStart
 * @create 2022-04-12 16:43
 */

public class WeeklyLog implements Cloneable
    private Attachment attachment;
    private String name;
    private Date date;
    private String content;

    public Attachment getAttachment() 
        return attachment;
    

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

    public String getName() 
        return name;
    

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

    public Date getDate() 
        return date;
    

    public void setDate(Date date) 
        this.date = date;
    

    public String getContent() 
        return content;
    

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


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

3.客户端测试类

**
 * 客户端测试类
 * @author WxrStart
 * @create 2022-04-12 16:45
 */

public class Client 
    public static void main(String[] args) 

        WeeklyLog preLog,newLog;
        Attachment attachment = new Attachment();
        attachment.setName("week 1 attachment");
        preLog = new WeeklyLog();
        preLog.setContent("周报week1的内容");
        preLog.setDate(new Date(System.currentTimeMillis()-7*24*3600*1000));
        preLog.setName("周报week1");
        preLog.setAttachment(attachment);
        //浅克隆新的对象newLog
        newLog = preLog.clone();
        //这里就是判断通过克隆方式创建的对象是否是全新的对象,false为全新对象
        System.out.println("周报地址(克隆的对象地址)是否相同?" + (preLog == newLog));
        System.out.println("*********************************************************");
        //通过下面几行代码,证实浅拷贝的特点
        System.out.println("preLog和newLog的未更改之前的Attachment的Name");
        System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
        System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
        //给newLog设置新的Attachment对象的属性Name
        newLog.getAttachment().setName("week 2 attachment");
        System.out.println("preLog和newLog的更改之后的Attachment的Name");
        System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
        System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
        System.out.println("*********************************************************");
        System.out.println("preLog和newLog的未更改之前的date属性");
        System.out.println("preLog的date: "+preLog.getDate());
        System.out.println("newLog的date: "+newLog.getDate());
        //给newLog设置新的Attachment对象的属性Name
        newLog.setDate(new Date());
        System.out.println("preLog和newLog的更改之后的date属性");
        System.out.println("preLog的date: "+preLog.getDate());
        System.out.println("newLog的date: "+newLog.getDate());

    

测试结果:

很显然,浅拷贝成功了

1.被preLog克隆出来的newLog对象地址不一样。

2.复制对象时仅仅复制对象本身,包括基本属性(Date),但该对象的属性引用其他对象时(Attachment),该引用对象不会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个。所以Date修改后preLog和newLog的Date值不一样,但是Attachment的name值修改后会一样。

6.2.2深克隆具体代码实现:

很显然,在clone方法的时候有问题了,所以我们需要重新修改clone方法,但是如果还用clone的话很难做到,所以我们选择使用Java自带的序列化机制

1.附件类Attachment

/**
 * 附件类
 */
public class Attachment implements Serializable 
    private String name;

    public String getName() 
        return name;
    

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

    public void download() 
        System.out.println("下载附件,文件名为:" + this.name);
    


2.原型类WeeklyLog

/**
 * WeeklyLog原型类
 * @author WxrStart
 * @create 2022-04-12 16:43
 */

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

    public Attachment getAttachment() 
        return attachment;
    

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

    public String getName() 
        return name;
    

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

    public Date getDate() 
        return date;
    

    public void setDate(Date date) 
        this.date = date;
    

    public String getContent() 
        return content;
    

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


    //使用序列化技术实现深克隆
    public WeeklyLog deepClone() throws  Exception
        //1.创建一个字节数组输出流
        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        //2.用对象输入流包装
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        //3.将调用该方法的对象写出
        oos.writeObject(this);

        //4.创建一个字节数组输入流,读取刚刚输出到字节数组的对象this
        ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
        //5.用对象输入流包装
        ObjectInputStream ois=new ObjectInputStream(bis);
        //6.返回深克隆对象
        return (WeeklyLog)ois.readObject;
    

3.客户端测试类

**
 * 客户端测试类
 * @author WxrStart
 * @create 2022-04-12 16:45
 */
public class Client 
    public static void main(String[] args) throws Exception 

        WeeklyLog preLog,newLog;
        Attachment attachment = new Attachment();
        attachment.setName("week 1 attachment");
        preLog = new WeeklyLog();
        preLog.setContent("周报week1的内容");
        preLog.setDate(new Date(System.currentTimeMillis()-7*24*3600*1000));
        preLog.setName("周报week1");
        preLog.setAttachment(attachment);
        //浅克隆新的对象newLog
        newLog = preLog.deepClone();
        //这里就是判断通过克隆方式创建的对象是否是全新的对象,false为全新对象
        System.out.println("周报地址(克隆的对象地址)是否相同?" + (preLog == newLog));
        System.out.println("*********************************************************");
        //通过下面几行代码,证实浅拷贝的特点
        System.out.println("preLog和newLog的未更改之前的Attachment的Name");
        System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
        System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
        //给newLog设置新的Attachment对象的属性Name
        newLog.getAttachment().setName("week 2 attachment");
        System.out.println("preLog和newLog的更改之后的Attachment的Name");
        System.out.println("preLog的Attachment的Name: "+preLog.getAttachment().getName());
        System.out.println("newLog的Attachment的Name: "+newLog.getAttachment().getName());
        System.out.println("*********************************************************");
        System.out.println("preLog和newLog的未更改之前的date属性");
        System.out.println("preLog的date: "+preLog.getDate());
        System.out.println("newLog的date: "+newLog.getDate());
        //给newLog设置新的Attachment对象的属性Name
        newLog.setDate(new Date());
        System.out.println("preLog和newLog的更改之后的date属性");
        System.out.println("preLog的date: "+preLog.getDate());
        System.out.println("newLog的date: "+newLog.getDate());
    

测试结果:

很显然,深拷贝成功了

1.被preLog克隆出来的newLog对象地址不一样。

2.复制对象时不光复制对象本身,包括基本属性(Date)和该对象的属性引用其他对象 (Attachment),该引用对象也会被复制,即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象不是同一个。所以Date修改后preLog和newLog的Date值不一样,但是Attachment的name值修改后也会不一样。

JAVA设计模式 2创建型原型模式的理解与使用

在本节中,我们将学习和使用原型模式;这一节学习的原型模式也是创建型 模式的其中之一。再次复习一下:创建型 模式就是描述如何去更好的创建一个对象。

我们都知道,在JAVA 语言中。使用new 关键字创建一个新对象。将新的对象放到堆内存 里面。当然,这个内存肯定是有大小限制的,况且,JAVA 不同于C语言等。 有内存管理机制,就是我们常说的垃圾回收器GC,才可以保证内存不被溢出。

说这些其实就是为了表示:为啥要用单例模式,能节省内存的时候,能用一个对象解决重复的事情,绝对不会创建多个。

概述

原型模式描述的如何快速创建重复的对象,并且减少new 关键字的使用。

  • 抽象原型类
  • 具体原型类
  • 访问类

容我来一个一个解释:

抽象原型类 也就是我们具体要实现的某个类,这个类在JAVA 里面是有具体的接口的,其实是一个空接口,Cloneable

 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

我们会发现,这个类没有任何的方法,怎么来实现它,不要慌。先接着走。

具体原型类 也就是我们具体要克隆 的对象。比如我们重复的要创建100个学生Student 对象,那么具体的学生对象就是具体原型类

public class Student implements Cloneable {

    private int id;

    private String name;

    private int sex;
}

访问类 我就不必多说了

浅克隆和深克隆

原型模式其实也分浅克隆和深克隆。如何理解这两个概念呢?

浅克隆

protected native Object clone() throws CloneNotSupportedException;

浅克隆,只需要具体原型类 实现Cloneable 接口,并且重写父类Object类的clone() 方法,即可实现对象的浅克隆。

Student student1 = new Student(1, "李四");
Student student2 = student1.clone();

System.out.println(student1);
System.out.println(student2);

System.out.println(student1 == student2);
---------------------
学号:1,姓名:李四
学号:1,姓名:李四
false
  • 通过执行clone() 方法即可创建一个相同的,具有同样属性的对象。
  • 并且是新的对象,内存地址有所不同。

我们来看看,对于引用类型的变量,浅克隆是否可以进行克隆;

Teacher teacher = new Teacher(1, "张老师");

Student student1 = new Student(1, "李四", teacher);
Student student2 = student1.clone();

System.out.println(student1);
System.out.println(student2);

System.out.println(student1 == student2);
------------
学号:1,姓名:李四,老师=Teacher@1b6d3586
学号:1,姓名:李四,老师=Teacher@1b6d3586
false

我们发现,引用类型并没有被克隆,也就是说:

特点

  • 浅克隆对于基本类型,可以进行完全的克隆,并且克隆的对象是一个新的对象
  • 但是对象里面的引用,是无法被克隆的。

深克隆(序列化)

何谓序列化?

我们创建的都是保存在内存里面的,只要被虚拟机GC进行回收,那么这个对象的任何属性都是消失,我们能不能找一个方法,将内存中这种对象的属性以及对象的状态通过某种东西保存下来,比如保存到数据库,下次从数据库将这个对象还原到内存里面。 这就是序列化。

  • 序列化 内存对象->序列字符
  • 反序列化 序列字符->内存对象

请参考: https://baike.baidu.com/item/序列化/2890184

JAVA 序列化

 * @see java.io.Externalizable
 * @since   JDK1.1
 */
public interface Serializable {
}

JAVA 提供了一个空接口,其实这个接口和上面的Cloneable 一样,都是一个空接口,其实这个空接口就是作为一种标识 你的对象实现了这个接口,JAVA 认为你的这个就可以被序列化 ,就是这么简单。

Teacher teacher = new Teacher(1, "张老师");

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(outputStream);

stream.writeObject(teacher);
System.out.println(Arrays.toString(outputStream.toByteArray()));
----------
[-84, -19, 0, 5, 115, 114, 0, 7, 84, 101, 97,。。。。。。

通过将对象序列化、其实也就是将内存中的对象转化为二进制 字节数组

反序列化

Teacher teacher = new Teacher(1, "张老师");
System.out.println(teacher);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(outputStream);

stream.writeObject(teacher);
System.out.println(Arrays.toString(outputStream.toByteArray()));

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(outputStream.toByteArray());
ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);

Teacher teacher1 = (Teacher) inputStream.readObject();
System.out.println(teacher1);
---------------
id=1,name=张老师
[-84, -19, 0, 5, 115, xxxxx,-127, -27, -72, -120]
id=1,name=张老师

通过序列化和反序列化,即可对象的深克隆

小结

这一节,在讲述 原型模式的同时,将原有实现原型模式的clone() 浅克隆,延伸到深克隆这一概念。其实JAVA 的原型模式,实现起来较为简单。但还是要按需要实现,Object 类提供的 clone 浅克隆 是没办法克隆对象的引用类型的。需要克隆引用类型,还是需要序列化 深克隆

参考

http://c.biancheng.net/view/1343.html
https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698

代码示例

https://gitee.com/mrc1999/Dev-Examples

欢迎关注

技术图片


以上是关于Java设计模式-创建型设计模式-原型模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 创建型模式篇:原型模式

从零开始学习Java设计模式 | 创建型模式篇:原型模式

Java23种设计模式之创建型模式「原型模式」

创建型设计模式

创建型设计模式 之 原型模式

创建型模式之原型模式