6.原型模式【Prototype Pattern】,什么是原型模式?特性?优缺点?使用场景?原型模式实现?浅拷贝和深拷贝?

目录


设计模式专栏目录(点击进入…)



什么是原型模式?

原型模式(Prototype Pattern)是一种用于创建重复的对象,同时又能保证性能的设计模式。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。或者说:原型模式指原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

基本介绍
用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。

是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节

原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝他们自己来实施创建,即对象.clone();。

理解:孙悟空拔出猴毛,变成其他孙悟空。

注意:原型模式的核心在于如何实现克隆方法。


原型模式包含角色?

(1)Prototype(抽象原型类)

声明克隆方法的接口,是具体原型类的公共父类,可以是抽象类也可以是接口,还可以是具体实现类。

(2)ConcretePrototype(具体原型类)

实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

(3)Client(客户类/访问类)

让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要实例化或通过工厂方法创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象,由于客户类针对抽象原型类Prototype编程,因此可以根据需要选择具体原型类,使用具体原型类中的 clone() 方法来复制新的对象。


原型模式的浅克隆和深克隆

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

在JDK中的Object类中提供了clone()方法来实现浅克隆。Cloneable接口是抽象原型,而实现了Cloneable接口的子实现类就是具体的原型类。


原型模式的特性?

(1)意图

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

(2)主要解决

在运行期建立和删除原型。

(3)何时使用:

1、当一个系统应该独立于它的产品创建,构成和表示时。
2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。
3、为了避免创建一个与产品类层次平行的工厂类层次时。
4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

(4)如何解决

利用已有的一个原型对象,快速地生成和原型对象一样的实例。

(5)关键代码:

1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),或通过序列化的方式来实现深拷贝。
2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。

(5)注意事项:

与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。


原型模式优缺点

(1)优点

1、性能提高。
2、逃避构造函数的约束。

(2)缺点

1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable 接口。


原型模式适合在什么场景使用?

1、资源优化场景。
2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
3、性能和安全要求的场景。
4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
5、一个对象多个修改者的场景。
6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。

在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone的方法创建一个对象,然后由工厂方法提供给调用者。 原型模式先产生出一个包含大量共有信息的类,然后可以拷贝出副本,修正细节信息,建立了一个完整的个性对象。不知道大家有没有看过施瓦辛格演的《第六日》这个电影,电影的主线也就是一个人被复制,然后正本和副本对掐,我们今天讲的原型模式也就是由一个正本可以创建多个副本的概念,可以这样理解一个对象的产生可以不由零开始,直接从一个已经具备一定雏形的对象克隆,然后再修改为一个生产需要的对象。也就是说,产生一个人,可以不从 1 岁 长到 2 岁,再 3 岁…,也可以直接找一个人,从其身上获得 DNS,然后克隆一个,直接修改一下就是 3 岁的了!,我们讲的原型模式也就是这样的功能,是紧跟时代潮流的。


原型模式实现

通用实现方法

在这里插入图片描述

1、原型克隆接口(抽象原型类)

package com.uhhe.common.design.prototype;

/**
 * 原型克隆接口
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
public interface Prototype {

    /**
     * 克隆
     *
     * @return 接口
     */
    Prototype clone();

}

2、具体原型类

在具体原型类的克隆方法中实例化一个与自身类型相同的对象将其返回,并将相关参数传入新创建的对象中,保证成员属性相同。

package com.uhhe.common.design.prototype;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 具体原型类
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
@Data
@NoArgsConstructor
public class ConcretePrototype implements Prototype {

    /**
     * 成员属性
     */
    private String attr;

    @Override
    public Prototype clone() {
        ConcretePrototype concretePrototype = new ConcretePrototype();
        concretePrototype.setAttr(this.attr);
        return concretePrototype;
    }

}

3、访问类

在客户类中创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象

package com.uhhe.common.design.prototype;

/**
 * 客户端/访问类
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
public class PrototypeClient {
    
    public static void main(String[] args) {
        ConcretePrototype concretePrototype = new ConcretePrototype();
        System.out.println(concretePrototype);
        concretePrototype.setAttr("通用克隆");
        Prototype clonedPrototype = concretePrototype.clone();
        System.out.println(clonedPrototype);
    }

}

使用clone()方法

在 Java 中可以直接使用 Object 提供的clone()方法来实现对象的克隆。

注意:实现克隆的 Java 类必须实现 Cloneable 接口,表示这个 Java 类支持被复制。

Java 的 clone() 方法满足
(1)对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
(2)对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
(3)如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立;

利用 Object 类的 clone() 方法,步骤如下
(1)在派生类中覆盖基类的clone()方法,并声明为public;
(2)在派生类的clone()方法中,调用super.clone();
(3)派生类需实现Cloneable接口。

此时,Object类相当于抽象原型类,实现了Cloneable接口的类相当于具体原型类。

1、原型克隆接口(抽象原型类)

package com.uhhe.common.design.prototype;

/**
 * 原型克隆接口
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
public interface Prototype {

    /**
     * 克隆
     *
     * @return 接口
     */
    Prototype clone();

}

2、具体原型类

package com.uhhe.common.design.prototype;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 具体原型类
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
@Data
@NoArgsConstructor
public class ConcretePrototype implements Cloneable, Prototype {

    /**
     * 成员属性
     */
    private String attr;

    @Override
    public Prototype clone() {
        Object object = null;
        try {
            object = super.clone();
        } catch (CloneNotSupportedException exception) {
            System.err.println("Not support cloneable");
        }
        return (Prototype) object;
    }

}

3、访问类

在客户类中创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象

package com.uhhe.common.design.prototype;

/**
 * 客户端/访问类
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
public class PrototypeClient {

    public static void main(String[] args) {
        ConcretePrototype concretePrototype = new ConcretePrototype();
        System.out.println(concretePrototype);
        Prototype clonedPrototypeCopy = concretePrototype.clone();
        System.out.println(clonedPrototypeCopy);
    }

}

实际案例(电子邮件)

现在电子账单越来越流行了,比如你的信用卡,到月初的时候银行就会发一份电子邮件到你邮箱中,
说你这个月消费了多少,什么时候消费的,积分是多少等等,这个是每个月发一次,但是还有一种也是银行发的邮件你肯定有印象:广告信,现在各大银行的信用卡部门都在拉拢客户,电子邮件是一种廉价、快捷的通讯方式,你用纸质的广告信那个费用多高呀,比如我今天推出一个信用卡刷卡抽奖活动,通过电子账单系统可以一个晚上发送给 600 万客户。

为什么要用电子账单系统呢?直接找个发垃圾邮件不就解决问题了吗?
是个好主意,但是这个方案在金融行业是行不通的,银行发这种邮件是有要求的,一是一般银行都要求个性化服务,发过去的邮件上总有一些个人信息吧,比如“XX 先生”,“XX 女士”等等,二是邮件的
到达率有一定的要求,由于大批量的发送邮件会被接收方邮件服务器误认是垃圾邮件,因此在邮件头要增加一些伪造数据,以规避被反垃圾邮件引擎误认为是垃圾邮件;从这两方面考虑广告信的发送也是电子账单系统(电子账单系统一般包括:账单分析、账单生成器、广告信管理、发送队列管理、发送机、退信处理、报表管理等)的一个子功能,我们今天就来考虑一下广告信这个模块是怎么开发的。那既然是广告信,肯定需要一个模版,然后再从数据库中把客户的信息一个一个的取出,放到模版中生成一份完整的邮件,然后扔给发送机进行发送处理,我们来看类图:

在这里插入图片描述

1、邮件信息(原型类,Mail实现Cloneable)

package com.uhhe.common.design.prototype;

import lombok.Data;

/**
 * 邮件信息
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
@Data
public class Mail implements Cloneable {
    /**
     * 收件人
     */
    private String receiver;
    /**
     * 邮件名称
     */
    private String subject;
    /**
     * 称谓
     */
    private String appellation;
    /**
     * 邮件内容
     */
    private String contxt;
    /**
     * 邮件的尾部,一般都是加上“XXX版权所有”等信息
     */
    private String tail;

    /**
     * 构造函数
     *
     * @param advTemplate 模版
     */
    public Mail(AdvTemplate advTemplate) {
        this.contxt = advTemplate.getAdvContext();
        this.subject = advTemplate.getAdvSubject();
    }

    @Override
    public Mail clone() {
        Mail mail = null;
        try {
            mail = (Mail) super.clone();
        } catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return mail;
    }

}

2、邮件模板信息

package com.uhhe.common.design.prototype;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 广告信模版
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdvTemplate {

    /**
     * 广告信名称
     */
    private String advSubject ="XX银行国庆信用卡抽奖活动";

    /**
     * 广告信内容
     */
    private String advContext = "国庆抽奖活动通知:只要刷卡就送你1百万!....";

}

3、邮件发送(访问类)

package com.uhhe.common.design.prototype;

import java.util.Random;

/**
 * 邮件发送访问类
 *
 * @author nizhihao
 * @version 1.0.0
 * @date 2023/2/27 9:38
 */
public class MailClient {

    /**
     * 发送账单的数量,这个值是从数据库中获得
     */
    private static int MAX_COUNT = 6;

    public static void main(String[] args) {
        // 模拟发送邮件
        int i = 0;
        //把模板定义出来,这个是从数据库中获得
        Mail mail = new Mail(new AdvTemplate());
        mail.setTail("XX银行版权所有");
        while (i < MAX_COUNT) {
            //以下是每封邮件不同的地方
            //以下是每封邮件不同的地方
            Mail cloneMail = mail.clone();
            cloneMail.setAppellation(getRandString(5) + " 先生(女士)");
            cloneMail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
            //然后发送邮件
            sendMail(cloneMail);
            i++;
        }
    }

    // 发送邮件
    public static void sendMail(Mail mail) {
        System.out.println("标题:" + mail.getSubject() + "\t收件人:" + mail.getReceiver() + "\t....发送成功!");
    }

    //获得指定长度的随机字符串
    public static String getRandString(int maxLength) {
        String source = "abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        StringBuffer sb = new StringBuffer();
        Random rand = new Random();
        for (int i = 0; i < maxLength; i++) {
            sb.append(source.charAt(rand.nextInt(source.length())));
        }
        return sb.toString();

    }

}

运行结果不变,一样完成了电子广告信的发送功能,而且 sendMail 即使是多线程也没有关系,看到
mail.clone()这个方法了吗?把对象拷贝一份,产生一个新的对象,和原有对象一样,然后再修改细节的数据,如设置称谓,设置收件人地址等等。这种不通过 new 关键字来产生一个对象,而是通过对象拷贝来实现的模式就叫做原型模式

这个模式的核心是一个 clone 方法,通过这个方法进行对象的拷贝,Java 提供了一个 Cloneable 接口
来标示这个对象是可拷贝的,为什么说是“标示”呢?翻开 JDK 的帮助看看 Cloneable 是一个方法都没有的,这个接口只是一个标记作用,在 JVM 中具有这个标记的对象才有可能被拷贝,那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是覆盖 clone()方法,重写 clone()方法。

在 clone()方法上增加了一个注解@Override,没有继承一个类为什么可以重写呢?在 Java 中所有类的老祖宗是谁?对嘛,Object 类,每个类默认都是继承了这个类。


对象拷贝时,类的构造函数是不会被执行的

对象拷贝时,类的构造函数是不会被执行的。一个实现了 Cloneable 并重写了 clone 方法的类 A,有一个无参构造或有参构造 B,通过 new 关键字产生了一个对象 S,再然后通过 S.clone()方式产生了一个新的对象 T,那么在对象拷贝时构造函数 B 是不会被执行的。

来写一小段程序来说明这个问题:

public class Thing implements Cloneable {

    public Thing() {
        System.out.println("构造函数被执行了...");
    }

    @Override
    public Thing clone() {
        Thing thing = null;
        try {
            thing = (Thing) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return thing;
    }
    
}

再来写一个 Client 类,进行对象的拷贝:

public class Client {
    public static void main(String[] args) {
        //产生一个对象
        Thing thing = new Thing();

        //拷贝一个对象
        Thing cloneThing = thing.clone();
    }
}

运行结果如下:

构造函数被执行了...

对象拷贝时确实构造函数没有被执行,这个从原理来讲也是可以讲得通的,Object 类的 clone 方法的
原理是从内存中(具体的说就是堆内存)以二进制流的方式进行拷贝,重新分配一个内存块,那构造函数没有被执行也是非常正常的了。


浅拷贝和深拷贝

在解释什么是浅拷贝什么是拷贝前,先来看个例子:

import java.util.ArrayList;

public class Thing implements Cloneable {
    /**
     * 定义一个私有变量
     */
    private ArrayList<String> arrayList = new ArrayList<String>();

    @Override
    public Thing clone() {
        Thing thing = null;
        try {
            thing = (Thing) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return thing;
    }

    //添加arrayList的值
    public void setValue(String value) {
        this.arrayList.add(value);
    }

    //取得arrayList的值
    public ArrayList<String> getValue() {
        return this.arrayList;
    }
}

在 Thing 类中增加一个私有变量 arrayList,类型为 ArrayList,然后通过 setValue 和 getValue 分别进
行设置和取值,来看场景类:

public class Client {
    public static void main(String[] args) {
        //产生一个对象
        Thing thing = new Thing();
        //设置一个值
        thing.setValue("张三");

        //拷贝一个对象
        Thing cloneThing = thing.clone();
        cloneThing.setValue("李四");

        System.out.println(thing.getValue());
    }
}

猜想一下运行结果应该是什么?是就一个“张三”吗?运行结果如下:

[张三, 李四]

怎么会有李四呢?
是因为 Java 做了一个偷懒的拷贝动作,Object 类提供的方法 clone 只是拷贝本对象,其对象内部的数组、引用对象等都不拷贝,还是指向原生对象的内部元素地址,这种拷贝就叫做浅拷贝,确实是非常浅,两个对象共享了一个私有变量,你改我改大家都能改,是一个种非常不安全的方式,在实际项目中使用还是比较少的。

可能会比较奇怪,为什么在 Mail 那个类中就可以使用使用 String 类型,而不会产生由浅拷贝带来的问题呢?
内部的数组和引用对象才不拷贝,其他的原始类型比如int,long,String(Java 就希望你把 String 认为是基本类型,String 是没有 clone 方法的)等都会被拷贝的。

浅拷贝是有风险的,那怎么才能深入的拷贝呢?
修改一下我们的程序:

import java.util.ArrayList;

public class Thing implements Cloneable {
    /**
     * 定义一个私有变量
     */
    private ArrayList<String> arrayList = new ArrayList<String>();

    @Override
    public Thing clone() {
        Thing thing = null;
        try {
            thing = (Thing) super.clone();
            // +数组克隆
            thing.arrayList = (ArrayList<String>) this.arrayList.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return thing;
    }

}

仅仅增加了黄色部分,Client 类没有任何改变,运行结果如下:

[张三] 

这个实现了完全的拷贝,两个对象之间没有任何的瓜葛了,你修改你的,我修改我的,不相互影响,
这种拷贝就叫做深拷贝,深拷贝还有一种实现方式就是通过自己写二进制流来操作对象,然后实现对象的深拷贝,这个大家有时间自己实现一下。

深拷贝和浅拷贝建议不要混合使用,一个类中某些引用使用深拷贝某些引用使用浅拷贝,这是一种非
常差的设计,特别是是在涉及到类的继承,父类有几个引用的情况就非常的复杂,建议的方案深拷贝和浅拷贝分开实现。

Clone 与 final

Clone 与 final 两对冤家。对象的 clone 与对象内的 final 属性是由冲突的。

举例来说明这个问题:

import java.util.ArrayList;

public class Thing implements Cloneable {
    //定义一个私有变量
    private final ArrayList<String> arrayList = new ArrayList<String>();

    @Override
    public Thing clone() {
        Thing thing = null;
        try {
            thing = (Thing) super.clone();
            this.arrayList = (ArrayList<String>) this.arrayList.clone();
        } catch (
                CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return thing;
    }

}

黄色的部分仅仅增加了一个 final 关键字,然后编译器就报灰色的部分错误,正常呀,final 类型你还
想重写设值呀!完蛋了,你要实现深拷贝的梦想在 final 关键字的威胁下破灭了,路总是有的,我们来想想怎么修改这个方法:删除掉 final 关键字,这是最便捷最安全最快速的方式,你要使用 clone 方法就在类的成员变量上不要增加 final 关键字

  • 25
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未禾

您的支持是我最宝贵的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值