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

Posted 李阿昀

tags:

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

在本讲,我们来学习一下创建型模式里面的第四个设计模式,即原型模式。

概述

原型模式就是指用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

这段话读起来有点绕,是不是?没有关系,这里我会举二个例子来给大家解释一下。

第一个例子,大家还记得很早以前的一个新闻——克隆羊吧!如果我们要去克隆羊,首先得有一个真实存在的羊,是不是啊,那么真实存在的这个羊就是原型对象,而根据这个原型对象克隆出来和它一模一样的克隆羊就是新对象。

第二个例子,喜欢看电影的朋友,经常会看到电影里面有这样的一些场景,偷天大盗去偷盗一件宝物时,总是会提前打造一件宝物的复制品(或者仿制品),以到达一个偷天换日、神不知鬼不觉的目的。如果大盗要去仿制宝物的话,首先得有一个真品,然后他再根据真品去做一个和真品一模一样的仿制品,那么在这种情景下,原型对象就是真品,复制出来的新对象就是仿制品。

结构

原型模式包含有如下角色:

  • 抽象原型类:规定了具体原型对象必须实现的clone方法,该clone方法就是克隆的意思
  • 具体原型类:实现抽象原型类中的clone方法,它是可被复制(克隆)的对象
  • 访问类:使用具体原型类中的clone方法来复制(克隆)新的对象

知道原型模式里面包含的具体角色之后,咱们来看一下下面这个类图。

从以上类图中可以看到,有一个Prototype接口,它就属于抽象原型类,而且它里面还定义有一个clone方法,通过该方法复制出来的就是Prototype接口类型的对象。此外,我们还能看到Prototype接口有一个子实现类,即Realizetype,它就属于具体原型类,从上能看到该子实现类重写了父接口里面的clone方法。最后,我们来看一下访问类,即PrototypeTest,它就是一个测试类,里面有个主方法,主方法里面的测试代码是这样写的:首先创建一个原型对象,因为我们是要根据这个原型对象去创建(或者复制)一个新的对象的,然后调用该原型对象里面的clone方法复制出来一个和原型对象一模一样的新对象。

实现

下面我们就要开始编写代码来实现以上类图所表示的案例了。不过在这之前,大家还得清楚如下概念。

原型模式的克隆分为浅克隆和深克隆:

  • 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本数据类型属性(即引用数据类型属性),仍指向原有属性所指向的对象的内存地址
  • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址

大家初次看到浅克隆和深克隆这两个概念,想必理解的不是很深刻,但是没关系,下面我会通过代码来给大家演示一下。

注意,现在我们所讲述的原型模式指的就是浅克隆。至于深克隆,我等下会给大家进行一个扩展。

此外,我们还应知道一点,在Java里面,Object类提供了clone方法来实现浅克隆,注意是浅克隆哟!而且,在Java里面还有一个接口,即Cloneable,你可以把它理解成原型模式中的抽象原型类,这样,实现了该接口的子实现类就是具体原型类了。所以,等会我们编写代码时,就将以上类图中的Prototype接口换成Cloneable接口,这样做还是有好处的,因为Cloneable接口在Java里面已经定义好了,我们直接拿过来用就行了,而不需要再重复的去定义了。

清楚以上概念之后,接下来,我们就要开始编写代码来实现了。

首先,打开咱们的maven工程,在com.meimeixia.pattern包下新建一个子包,即prototype.demo,使用原型模式实现以上类图所表示的案例的代码我们都放在了该包下。

然后,创建具体原型类,即Realizetype,注意,该类得去实现Cloneable接口并重写它里面的clone方法。

package com.meimeixia.pattern.prototype.demo;

/**
 * @author liayun
 * @create 2021-06-02 6:15
 */
public class Realizetype implements Cloneable 
    public Realizetype() 
        System.out.println("具体的原型对象创建完成!");
    

    /**
     *
     * @return 克隆出来的,我们明确肯定是该具体原型类的对象,所以我们应把clone方法的返回值类型改成Realizetype
     * @throws CloneNotSupportedException
     */
    @Override
    public Realizetype clone() throws CloneNotSupportedException 
        System.out.println("具体原型复制成功!");
        return (Realizetype) super.clone();
    

接着,创建访问类,这里我们不妨将其命名为Client。根据以上类图,相信你能写出下面这样的测试代码。

package com.meimeixia.pattern.prototype.demo;

/**
 * @author liayun
 * @create 2021-06-02 6:30
 */
public class Client 
    public static void main(String[] args) throws CloneNotSupportedException 
        // 创建一个原型类对象
        Realizetype realizetype = new Realizetype();
        // 调用原型类(即Realizetype)中的clone方法进行对象的克隆
        Realizetype clone = realizetype.clone();
        System.out.println("原型对象和克隆出来的是否是同一个对象呢?" + (realizetype == clone));
    

最后运行以上测试类看结果,如下图所示,打印结果是false,即原型对象和克隆出来的对象不是同一个对象,而且你还能从中发现先是调用了Realizetype类的无参构造方法去创建对象,再是去调用clone方法复制对象,大家注意看哟,在复制对象时,有没有再去执行无参构造方法啊?没有,这说明底层不是通过new对象的方式去克隆一个新对象的,而是通过调用clone方法。

至此,我们就通过以上代码实现了克隆这样一个效果。

案例

接下来,我们通过一个原型模式的案例再来理解一下原型模式以及原型模式里面的浅克隆和深克隆这俩概念。

该案例就是使用原型模式生成三好学生奖状。

同一学校的"三好学生"奖状除了获奖人姓名不同之外,其他的都相同,那么这种情况下,我们就可以使用原型模式复制多个"三好学生"奖状出来,然后再修改奖状上的名字即可。根据分析,我们是不难画出下面这样的类图的。

从以上类图中可以看到,首先有一个抽象原型接口,即Cloneable,该接口并不需要我们去定义,因为在Java里面已经定义好了,我们直接用就可以。该接口的子实现类,即Citation,就是具体原型类,能看到我们还在该类中声明了一个name属性来记录获奖人的姓名,并且为其提供了相应的getter和setter方法,最后还重写了父接口中的clone方法。

大家不要忘了还有一个访问类哟,也就是测试类,测试类中只有一个主方法,主方法里面的测试代码是这样写的:首先创建一个原型对象,然后再调用它里面的clone方法克隆出来多个三好学生奖状。

分析至此,接下来,我们就要开始编写代码来实现以上案例了。

首先,在com.meimeixia.pattern.prototype包下新建一个子包,即test,使用原型模式实现以上案例的代码我们都放在了该包下。

然后,创建三好学生类,即Citation,注意,该类得去实现Cloneable接口并重写它里面的clone方法。

package com.meimeixia.pattern.prototype.test;

/**
 * @author liayun
 * @create 2021-06-02 6:43
 */
public class Citation implements Cloneable 

    // 三好学生上的姓名
    private String name;

    public String getName() 
        return name;
    

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

    /**
     *
     * @return 因为克隆出来的肯定是三号学生类的对象,所以我们要将clone方法的返回值类型修改为Citation
     * @throws CloneNotSupportedException
     */
    @Override
    public Citation clone() throws CloneNotSupportedException 
        return (Citation) super.clone();
    

    public void show() 
        System.out.println(name + "同学:在2021学年第一学期中表现优秀,被评为三好学生。特发此状!");
    


大家注意了,我们在以上类中还提供了一个show方法,这在类图中是没画出来的,之所以提供该方法,是因为等会我们在打印结果时,可以让大家看得更加清楚一点。

接着,创建访问类,这里我们就命名为CitationTest了。像下面这样的测试代码不难写吧!

package com.meimeixia.pattern.prototype.test;

/**
 * @author liayun
 * @create 2021-06-02 6:55
 */
public class CitationTest 
    public static void main(String[] args) throws CloneNotSupportedException 
        // 1. 创建原型对象
        Citation citation = new Citation();
        citation.setName("张三"); // 假设这张奖状是张三的

        // 2. 克隆奖状对象
        Citation citation1 = citation.clone();
        citation1.setName("李四"); // 克隆出来的这张奖状是李四的

        // 3. 调用show方法展示奖状
        citation.show();
        citation1.show();
    

最后,我们便要来运行以上访问类看结果了,如下图所示,第一个奖状是张三的,第二个奖状是李四的,打印的结果确实是符合我们的预期。

使用场景

我们能在哪些场景下使用原型模式呢?这里,我就直接告诉大家答案了,原型模式的使用场景如下:

  • 如果对象的创建非常复杂,那么可以使用原型模式快捷的创建对象,即使用原型对象进行克隆,这样,我们就不需要去关注对象创建的一些细节了。当然,前提是原型对象所属类必须实现Cloneable接口
  • 性能和安全要求比较高。在这种场景下,我们在设计类时不妨让它去实现Cloneable接口,如有需要,则让它去复制或者克隆新的对象,而不是再去new了。而且,在复制对象的过程中,还能按照我们自己的逻辑去创建对应的对象

扩展:深克隆

相信通过上面使用原型模式生成三好学生奖状的案例,大家对原型模式理解得更加深刻了。原型模式说到底本质就是克隆,只不过上面我讲述的都是浅克隆,接下来,我就来为大家详细讲一下深克隆。

在正式讲解深克隆之前,首先我们得做一件事情,即将上面的"三好学生"奖状的案例中Citation类的name属性修改为Student类型的属性,代码如下:

package com.meimeixia.pattern.prototype.test;

/**
 * @author liayun
 * @create 2021-06-02 6:43
 */
public class Citation implements Cloneable 

    /*
    // 三好学生上的姓名
    private String name;

    public String getName() 
        return name;
    

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

    private Student stu;

    public Student getStu() 
        return stu;
    

    public void setStu(Student stu) 
        this.stu = stu;
    

    /**
     *
     * @return 因为克隆出来的肯定是三号学生类的对象,所以我们要将clone方法的返回值类型修改为Citation
     * @throws CloneNotSupportedException
     */
    @Override
    public Citation clone() throws CloneNotSupportedException 
        return (Citation) super.clone();
    

    public void show() 
        System.out.println(stu.getName() + "同学:在2021学年第一学期中表现优秀,被评为三好学生。特发此状!");
    


其中,Student类的代码就很简单了,如下所示:

package com.meimeixia.pattern.prototype.test;

/**
 * @author liayun
 * @create 2021-06-02 7:12
 */
public class Student 
    // 学生姓名
    private String name;

    public String getName() 
        return name;
    

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

    @Override
    public String toString() 
        return "Student" +
                "name='" + name + '\\'' +
                '';
    

此时,测试类(即CitationTest)中的代码肯定会报错,这是毋庸置疑的,那我们就把之前的测试代码都注释掉,重新编写代码进行测试。

package com.meimeixia.pattern.prototype.test;

/**
 * @author liayun
 * @create 2021-06-02 6:55
 */
public class CitationTest 
    public static void main(String[] args) throws CloneNotSupportedException 
        /*
        // 1. 创建原型对象
        Citation citation = new Citation();
        citation.setName("张三"); // 假设这张奖状是张三的

        // 2. 克隆奖状对象
        Citation citation1 = citation.clone();
        citation1.setName("李四"); // 克隆出来的这张奖状是李四的

        // 3. 调用show方法展示奖状
        citation.show();
        citation1.show();
        */

        // 1. 创建原型对象
        Citation citation = new Citation();
        // 创建学生对象
        Student stu = new Student();
        stu.setName("张三");
        citation.setStu(stu);

        // 2. 克隆奖状对象
        Citation citation1 = citation.clone();
        Student stu1 = citation1.getStu();
        stu1.setName("李四");

        // 3. 调用show方法展示
        citation.show();
        citation1.show();
    

现在我们是不是通过浅克隆克隆出来了一个奖状对象啊?那么问题来了,大家觉得在浅克隆的过程中,能不能也把学生对象给克隆出来呢?肯定是能够的,要是有同学觉得不能够,那请你再好好看看浅克隆的概念。从以上代码中可以看到,奖状对象克隆出来之后,我们是将奖状中学生的姓名改成李四了。

不妨运行一下以上测试类,如下图所示,可以看到两个奖状打印的学生的姓名都是李四。

明明我们将new出来的奖状中的学生姓名设置为了张三,为什么最后打印出来的学生姓名却是李四呢?这就是浅克隆所存在的问题,详细点说就是,由于stu对象和stu1对象是同一个对象,所以将stu1对象中name属性值改为"李四"的话,那么两个Citation(奖状)对象中显示的学生姓名都将会是李四。

这就是浅克隆的效果,对具体原型类(即Citation)中的引用类型的属性进行引用的复制。

讲到这里,大家有必要再去回顾一下上面讲的浅克隆和深克隆的概念。回顾完之后,想必大家觉得这时候用深克隆会更好些,是不是啊?接下来,我们就来使用深克隆去解决浅克隆所存在的问题。如果大家要进行深克隆的话,那么在这里我给大家提供一种方案,即使用对象流去操作。

首先,在com.meimeixia.pattern.prototype包下新建一个子包,即test1,使用深克隆解决浅克隆所存在的问题的代码我们都放在了该包下。

然后,将以上test包中的类全部拷贝一份到test1包下,这主要是为了方便,因为我们将在此基础上进行修改。

上面讲过,深克隆我们是使用对象流来操作的,也就是把奖状对象直接写到文件中,或者说序列化到硬盘上,这样,我们在多次读取奖状对象时读取到的就是不同的对象了,当然了,奖状对象里面的学生对象也会是不同的对象。

正是由于我们现在要把Citation类的对象序列化到文件里面,所以就要求该类必须去实现序列化接口了。

package com.meimeixia.pattern.prototype.test1;

import java.io.Serializable;

/**
 * @author liayun
 * @create 2021-06-02 6:43
 */
public class Citation implements Cloneable, Serializable 

    /*// 三好学生上的姓名
    private String name;

    public String getName() 
        return name;
    

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

    private Student stu;

    public Student getStu() 
        return stu;
    

    public void setStu(Student stu) 
        this.stu = stu;
    

    @Override
    public Citation clone() throws CloneNotSupportedException 
        return (Citation) super.clone();
    

    public void show() 
        System.out.println(stu.getName() + "同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!");
    


而且,大家不要忘了,以上Citation类中还有一个Student类型的属性,这样当我们在去序列化Citation类的对象时,是不是意味着Student类型的对象也要被序列化到文件中啊?所以,Student类也需要实现序列化接口。

package com.meimeixia.pattern.prototype.test1;

import java.io.Serializable;

/**
 * @author liayun
 * @create 2021-06-02 7:12
 */
public class Student implements Serializable 
    // 学生姓名
    private String name;

    public String getName() 
        return name;
    

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

    @Override
    public String toString() 
        return "Student" +
                "name='" + name + '\\'' +
                '';
    

最后,回到测试类里面进行修改,按照上面的分析,相信你不难写出下面这样的测试代码。

package com.meimeixia.pattern.prototype.test1;

import java.io.*;

/**
 * @author liayun
 * @create 2021-06-02 6:55
 */
public class CitationTest 
    public static void main(String[] args) throws Exception 
        // 1. 创建原型对象
        Citation citation = new Citation();
        // 创建张三学生对象
        Student stu = new Student();
        stu.setName("张三");
        citation.setStu(stu);

        // 创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/robin/a.txt"));
        // 写对象
        oos.writeObject(citation);
        // 释放资源
        oos.close();

        // 创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/robin/a.txt"));
        // 读取对象
        Citation citation1 = (Citation) ois.readObject();
        // 释放资源
        ois.close();

        // 将克隆出来的奖状中的学生姓名改为李四
        Student stu1 = citation1.getStu();
        stu1.setName("李四");

        // 此时,原先的奖状中的学生姓名也会随之改为李四吗?
        citation.show();
        citation1.show();
    

此时,运行以上测试类,打印结果如下图所示,发现确实达到了我们所想要的效果,原先的奖状上展示出来的学生姓名是张三,而通过序列化以及反序列化的方式克隆出来的奖状上展示的学生姓名是李四,这就是深克隆的效果。

至此,原型模式我就给大家全部讲完了,尤其是大家一定要弄清楚浅克隆和深克隆,这个很重要。

以上是关于从零开始学习Java设计模式 | 创建型模式篇:原型模式的主要内容,如果未能解决你的问题,请参考以下文章

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

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

从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

从零开始学习Java设计模式 | 创建型模式篇:工厂方法模式

从零开始学习Java设计模式 | 创建型模式篇:工厂方法模式