从零开始学设计模式(三):原型模式(Prototype Pattern)

370 阅读9分钟

小知识,大挑战!本文正在参与“   程序员必备小知识   ”创作活动

作者的其他平台:

| CSDN:blog.csdn.net/qq_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概8287字,读完共需15分钟

1 前言

前面的一篇文章从零开始学设计模式(二):单例模式介绍了什么是单例模式以及单例模式的几种常见的实现方式。今天这篇文章接着介绍设计模式中的原型模式Prototype。

2 原型模式Prototype Pattern

1、什么是原型模式

原型模式(Prototype Pattern)是 Java 中最简单的设计模式之一,属于创建型模式。原型模式使用原型实例指定创建对象的种类,并且通过拷贝原型对象创建新的对象。原型模式实际上就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。这就好比模具的使用,我们可以通过螺丝的模具(原型实例)创建一个个具体的螺丝(新的对象),而不需要知道螺丝具体的创建过程。下图就很好的表现了原型模式使用的过程,图源自网络,侵删:

2、原型模式的优点

a、使用原型模型创建一个对象比直接new一个对象更有效率,因为new产生一个对象需要非常繁琐的数据准备或访问权限,原型模式则直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

b、原型模式隐藏了制造新实例的复杂性,使得创建对象就像复制粘贴一样简单,所以效率高。

3、原型模式的缺点

a、由于使用原型模式复制对象时不会调用类的构造方法,所以原型模式无法和单例模式组合使用,因为原型类需要将clone方法的作用域修改为public类型,那么单例模式的条件就无法满足了。

b、使用原型模式时不能有final对象。

c、Object类的clone方法只会拷贝对象中的基本数据类型,对于数组,引用对象等只能另行拷贝。这里涉及到深拷贝和浅拷贝的概念。

3 原型模式实现方式

前面说到原型模式的优点就是原型模式隐藏了制造新实例的复杂性,使得创建对象就像复制粘贴一样简单,所以效率高。 所以原型模式中实现起来最困难的地方就是内存复制操作,但是Java中就提供了clone()方法替我们做了绝大部分事情,不需要我们自己操作。所以原型模式的实现方式是:实现Cloneable接口和Object类中的clone方法:

1、实现Cloneable接口

克隆类似于new,但是又不同于new。new创建新的对象属性采用的是默认值;而克隆出的对象的属性值完全和原型对象相同,然后,再修改克隆对象的值,并且克隆出的新对象改变不会影响原型对象。

Cloneable接口的作用是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。

2、重写Object类中的clone方法

Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,原型类需要将clone方法的作用域修改为public类型。

栗子:

作为一个程序员需要女朋友,我们可以随时给自己new一个prefect的对象,我们知道作为女朋友,那么性别肯定是女的(当然也可能不是),但是如果换了女朋友之后姓名、年龄、身高、体重等,这些数据一般来说是不一样的,那么我们可以通过原型模式来给自己创建对象了:

定义一个女朋友类:

package com.jiangxia.Prototype;

/**
 * @Author: 江夏
 * @Date: 2021/10/24/9:17
 * @Description:
 */
public class PrototypeDemo1 implements Cloneable{
    //性别
    private String gender;
    //年龄
    private int age;
    //姓名
    private String name;
    //体重
    private int weight;
    //身高
    private int height;
    public PrototypeDemo1(String gender){
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public PrototypeDemo1 clone(){
        PrototypeDemo1 prototypeDemo1 = null;
        try{
            //Object类的clone方法来完成内存中复制数据
            prototypeDemo1 = (PrototypeDemo1) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return prototypeDemo1;
    }
}

测试下:

package com.jiangxia.Prototype;

/**
 * @Author: 江夏
 * @Date: 2021/10/24/9:27
 * @Description:
 */
public class PrototypeTest {
    public static void main(String[] args) {
        //对象性别肯定是女,这个是一致的不变的数据
        String gender = "女";
        PrototypeDemo1 prototypeDemo1 = new PrototypeDemo1(gender);
        //clone prototypeDemo1 并且设置clone部分的值,这块数据是可变的,每个地方不一样
        PrototypeDemo1 cloneprototypeDemo1 = prototypeDemo1.clone();
        cloneprototypeDemo1.setAge(20);
        cloneprototypeDemo1.setHeight(160);
        cloneprototypeDemo1.setWeight(50);
        cloneprototypeDemo1.setName("韩梅梅");
        PrototypeDemo1 cloneprototypeDemo2 = prototypeDemo1.clone();
        cloneprototypeDemo2.setAge(19);
        cloneprototypeDemo2.setHeight(168);
        cloneprototypeDemo2.setWeight(45);
        cloneprototypeDemo2.setWeight(45);
        cloneprototypeDemo2.setName("李华");
        System.out.println("第一个女朋友的数据是:她叫:"+cloneprototypeDemo1.getName()+";年龄:"+cloneprototypeDemo1.getAge()+";身高:"+cloneprototypeDemo1.getHeight()+";体重:"+cloneprototypeDemo1.getWeight()+";性别是:"+cloneprototypeDemo1.getGender());
        System.out.println("新女朋友的数据是:她叫:"+cloneprototypeDemo2.getName()+";年龄:"+cloneprototypeDemo2.getAge()+";身高:"+cloneprototypeDemo2.getHeight()+";体重:"+cloneprototypeDemo2.getWeight()+";性别肯定还是:"+cloneprototypeDemo2.getGender());
    }
}

那么你女朋友的结果如下:

4 深拷贝与浅拷贝

在原型模式中还有两个概念:深拷贝和浅拷贝,也叫深克隆和浅克隆!这里克隆和拷贝是一样的!

浅拷贝:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,而所有的对其他对象的引用都仍然指向原来的对象,所以这样是不安全的。

深拷贝:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。把引用的变量指向复制过的新对象,而不是原有的被引用的对象。

那么深拷贝如何具体实现呢?

基本数据类型和String能够自动实现深拷贝(值的复制),其他的引用类型可以让已实现Clonable接口的类中的属性也实现Clonable接口。深拷贝实现方式有两种:

1、重写 clone 方法来实现深拷贝

2:通过对象序列化实现深拷贝

还是上面的例子,先看第一种:

package com.jiangxia.Prototype;

import java.io.Serializable;

/**
 * @Author: 江夏
 * @Date: 2021/10/24/10:21
 * @Description: 深拷贝重写clone方法来实现深拷贝
 */
public class DeepClonePrototypeDemo2 implements  Cloneable{
    //年龄
    private int age;
    //姓名
    private String name;
    //体重
    private int weight;
    //身高
    private int height;

    //构造器
    public DeepClonePrototypeDemo2(int age, String name, int weight, int height) {
        this.age = age;
        this.name = name;
        this.weight = weight;
        this.height = height;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
package com.jiangxia.Prototype;

import java.io.Serializable;

/**
 * @Author: 江夏
 * @Date: 2021/10/24/10:24
 * @Description:
 */
public class DeepClonePrototypeDemo22 implements Cloneable,Serializable {
    //性别字段
    String gender;
    //其他数据引用类型
    DeepClonePrototypeDemo2 deepClonePrototypeDemo2;

    public DeepClonePrototypeDemo22(){
        super();
    }

    //深拷贝第一种方式:重写clone方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Object deepclone = null;
        //这里完成了对基本数据类型(属性)和 String 的克隆,也就是值的复制
        deepclone = super.clone();
        //这里进行对引用类型的属性的复制进行处理
        DeepClonePrototypeDemo22 deepClonePrototypeDemo22 = (DeepClonePrototypeDemo22) deepclone;
        deepClonePrototypeDemo22.deepClonePrototypeDemo2 = (DeepClonePrototypeDemo2) deepClonePrototypeDemo2.clone();
        return deepClonePrototypeDemo22;
    }

}

测试代码:

package com.jiangxia.Prototype;

/**
 * @Author: 江夏
 * @Date: 2021/10/24/10:36
 * @Description: 深拷贝测试代码
 */
public class DeepCloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        DeepClonePrototypeDemo22 deep = new DeepClonePrototypeDemo22();
        deep.gender = "女";
        deep.deepClonePrototypeDemo2 = new DeepClonePrototypeDemo2(21, "韩梅梅",50,168);

        //重写clone方法 完成深拷贝
        DeepClonePrototypeDemo22 deep2 = (DeepClonePrototypeDemo22) deep.clone();

        System.out.println("性别:" + deep.gender + ";姓名:" +deep.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep.deepClonePrototypeDemo2.getWeight()+"||||"+deep.deepClonePrototypeDemo2.hashCode());
        System.out.println("深拷贝后性别:" + deep.gender + ";姓名:" +deep2.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep2.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep2.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep2.deepClonePrototypeDemo2.getWeight()+"||||"+deep2.deepClonePrototypeDemo2.hashCode());


    }
}

结果如下:

可以发现其他属性都一样,但是hashcode的值已经变了,不一样,说明对象的地址不是同一个,引用的已经不是同一个对象了!

继续上面的例子,看看使用序列化实现深拷贝:

//深拷贝第一种方式:使用序列化和反序列化实现深复制
    public Object deepClone() {
        //创建流对象,需要继承Serializable接口
        ByteArrayOutputStream bos = null;
        ObjectOutputStream oos = null;
        ByteArrayInputStream bis = null;
        ObjectInputStream ois = null;
        try {
            //序列化
            bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
            //当前这个对象以对象流的方式输出
            oos.writeObject(this);

            //反序列化
            bis = new ByteArrayInputStream(bos.toByteArray());
            ois = new ObjectInputStream(bis);
            DeepClonePrototypeDemo22 copyObj = (DeepClonePrototypeDemo22) ois.readObject();

            return copyObj;
        }

        catch (Exception e) {
            return null;
        }
        finally {
            //关闭流
            try {
                bos.close();
                oos.close();
                bis.close();
                ois.close();
            }
            catch (Exception e2) {
                System.out.println(e2.getMessage());
            }
        }
    }

测试用例代码:

//序列化实现深拷贝
DeepClonePrototypeDemo22 p3 = (DeepClonePrototypeDemo22) deep.deepClone();
System.out.println("性别:" + deep.gender + ";姓名:" +deep.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep.deepClonePrototypeDemo2.getWeight()+"||||"+deep.deepClonePrototypeDemo2.hashCode());
System.out.println("第二种深拷贝后性别:" + deep3.gender + ";姓名:" +deep3.deepClonePrototypeDemo2.getName()+ ";年龄:" +deep3.deepClonePrototypeDemo2.getAge()+ ";身高:" +deep3.deepClonePrototypeDemo2.getHeight()+ ";体重:" +deep3.deepClonePrototypeDemo2.getWeight()+"||||"+deep3.deepClonePrototypeDemo2.hashCode());

运行结果如下:

5 常见应用场景

原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。在spring中bean的创建实际就是两种:单例模式和原型模式,并且原型模式需要和工厂模式搭配起来。

6 总结

以上就是我对于原型模式的一些简单的理解。

使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。

如果你觉得本文不错,就点赞分享给更多的人吧!

如果你觉得文章有不足之处,或者更多的想法和理解,欢迎指出讨论!

最后本文的测试代码都会同步至github:github.com/JiangXia-10…

其他推荐: