设计模式3-原型模式

原型模式定义

以下定义摘抄自:https://www.runoob.com/design-pattern/prototype-pattern.html

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式是实现一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。
例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。


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

主要解决
在运行期建立和删除原型

何时使用

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

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

关键代码

  1. 实现克隆操作,在Java继承Cloneable,重写clone方法
  2. 原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些“易变类”拥有稳定的接口。

优点

  1. 性能提高,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多
  2. 逃避构造函数的约束

缺点

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

使用场景

  1. 资源优化场景
  2. 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等
  3. 性能和安全要求的场景
  4. 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式
  5. 一个对象多个修改者的场景
  6. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象共调用者使用
  7. 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与java融为浑然一体,大家可以随手拿来使用

注意事项

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

原型模式实现

孙悟空拔下猴毛轻轻一吹就变出很多孙悟空,这实际上就用到了原型模式。
平时我们进行群发邮件或消息,也可能用到原型模式,以下为一个邮件发送的示例。

1、使用原型模式之前

一个邮件类,包含发送人、接收人等信息

/**
 * 邮件信息
 *
 * @author ccheng
 * @date 2021-12-18
 */
@Data
public class Email {
    /**
     * 发件人
     */
    private String fromUser;
    /**
     * 收件人
     */
    private String toUser;
    /**
     * 邮件主题
     */
    private String subject;
    /**
     * 邮件正文
     */
    private String context;
}

在没有实现原型模式之前,我们可能需要这么干

/**
 * 原型模式-没有使用原型
 *
 * @author ccheng
 * @date 2021/12/18
 */
@Slf4j
public class Client {
    public static void main(String[] args) {
        String subject = "今晚打老虎";
        String context = "三缺一,速来";
        String fromUser="ccheng.top@qq.com";
        //发给张三的邮件
        Email email1 = new Email();
        email1.setSubject(subject);
        email1.setContext(context);
        email1.setFromUser(fromUser);
        email1.setToUser("zhangsan@qq.com");
        //发给李四的邮件
        Email email2 = new Email();
        email2.setSubject(subject);
        email2.setContext(context);
        email2.setFromUser(fromUser);
        email2.setToUser("lisi@qq.com");
        //群发邮件
        List<Email> emails = new LinkedList<>();
        emails.add(email1);
        emails.add(email2);
        send(emails);
    }

    /**
     * 群发邮件
     */
    public static void send(List<Email> emails) {
        for (Email email : emails) {
            log.info("发送邮件:{}", email);
        }
    }
}

//运行结果
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=zhangsan@qq.com, subject=今晚打老虎, context=三缺一,速来)
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=lisi@qq.com, subject=今晚打老虎, context=三缺一,速来)

可以看到给张三和李四发送的邮件,除了收件人不一样,其他都是一样的。但是每个邮件都是通过new的方式产生,还要给相同的属性设置相同的属性值,如果属性很多或收件人很多的时候将是一件比较繁琐的事情。

2、使用原型模式之后

在jdk中已经有现成的原型模式API,所有的Java类都继承自java.lang.Object,事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。

注意:

  • Object类虽然有clone这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。
  • 使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。

以下将Email类实现java.lang.Cloneable接口,并复写clone()方法,就完成了最简单克隆功能

/**
 * 邮件信息
 *
 * @author ccheng
 * @date 2021-12-18
 */
@Data
public class Email implements Cloneable{
    /**
     * 发件人
     */
    private String fromUser;
    /**
     * 收件人
     */
    private String toUser;
    /**
     * 邮件主题
     */
    private String subject;
    /**
     * 邮件正文
     */
    private String context;

    @Override
    public Email clone() {
        try {
            Email clone = (Email) super.clone();
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

这时,客户端在创建邮件的时候,可以通过创建一个邮件模板(原型实例),然后通过clone()方法克隆多个邮件实例。

/**
 * 原型模式-使用原型
 *
 * @author ccheng
 * @date 2021/12/18
 */
@Slf4j
public class Client {
    public static void main(String[] args) {
        String subject = "今晚打老虎";
        String context = "三缺一,速来";
        String fromUser="ccheng.top@qq.com";
        //邮件信息模板(原型实例)
        Email template = new Email();
        template.setSubject(subject);
        template.setContext(context);
        template.setFromUser(fromUser);
        //发给张三的邮件
        Email email1 = template.clone();//通过clone方法得到新的邮件实例
        email1.setToUser("zhangsan@qq.com");
        //发给李四的邮件
        Email email2 = template.clone();//通过clone方法得到新的邮件实例
        email2.setToUser("lisi@qq.com");
        //群发邮件
        List<Email> emails = new LinkedList<>();
        emails.add(email1);
        emails.add(email2);
        send(emails);
    }

    /**
     * 群发邮件
     */
    public static void send(List<Email> emails) {
        for (Email email : emails) {
            log.info("发送邮件:{}", email);
        }
    }
}

//运行结果
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=zhangsan@qq.com, subject=今晚打老虎, context=三缺一,速来)
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=lisi@qq.com, subject=今晚打老虎, context=三缺一,速来)

从运行结果可以看到,结果和使用原型模式之前是一样的,除了收件人不一样,其他属性都是一样的。

那么真的这么简单就完事了吗?
当然没有,下面我们改一下邮件,增加一个邮件附件列表属性List attachments。

/**
 * 邮件信息
 *
 * @author ccheng
 * @date 2021-12-18
 */
@Data
public class Email implements Cloneable{
    /**
     * 发件人
     */
    private String fromUser;
    /**
     * 收件人
     */
    private String toUser;
    /**
     * 邮件主题
     */
    private String subject;
    /**
     * 邮件正文
     */
    private String context;
    /**
     * 邮件附件
     */
    private List<String> attachments;

    @Override
    public Email clone() {
        try {
            Email clone = (Email) super.clone();
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

给邮件模板添加几张“正经的图片”,然后发给张三的邮件偷偷追加一张“羞羞的图片”,但是不能给李四知道。

/**
 * 原型模式-使用原型
 *
 * @author ccheng
 * @date 2021/12/18
 */
@Slf4j
public class Client {
    public static void main(String[] args) {
        String subject = "今晚打老虎";
        String context = "三缺一,速来";
        String fromUser="ccheng.top@qq.com";
        //邮件信息模板(原型实例)
        Email template = new Email();
        template.setSubject(subject);
        template.setContext(context);
        template.setFromUser(fromUser);
        //添加几张正经的图片
        List<String> attachments = new LinkedList<>();
        attachments.add("正经的图片1");
        attachments.add("正经的图片2");
        template.setAttachments(attachments);
        //发给张三的邮件
        Email email1 = template.clone();//通过clone方法得到新的邮件实例
        email1.setToUser("zhangsan@qq.com");
        //给张三追加一张羞羞的图片,李四没份!!!!
        email1.getAttachments().add("羞羞的图片");
        //发给李四的邮件
        Email email2 = template.clone();//通过clone方法得到新的邮件实例
        email2.setToUser("lisi@qq.com");

        //群发邮件
        List<Email> emails = new LinkedList<>();
        emails.add(email1);
        emails.add(email2);
        send(emails);
    }

    /**
     * 群发邮件
     */
    public static void send(List<Email> emails) {
        for (Email email : emails) {
            log.info("发送邮件:{}", email);
        }
    }
}

//运行结果
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=zhangsan@qq.com, subject=今晚打老虎, context=三缺一,速来, attachments=[正经的图片1, 正经的图片2, 羞羞的图片])
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=lisi@qq.com, subject=今晚打老虎, context=三缺一,速来, attachments=[正经的图片1, 正经的图片2, 羞羞的图片])
    

但是,翻车了!!!说好的不给李四看的“羞羞的图片”,结果李四也收到了,天啊,怎么会这样。

3、浅复制和深复制

想要了解翻车的原因,我们先简单了解一下啥是浅复制和深复制。

数据类型
java的数据类型分为基本数据类型和引用数据类型。
对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

浅拷贝(Shallow Copy):

  1. 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
  2. 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
  3. String类型非常特殊。首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!如果对克隆对象的String属性进行修改,并不是修改了这个属性的值,而是把这个属性的引用指向了另外一个字符常量,对原型对象和其他克隆对象的属性都不会有影响。

深拷贝(Deep Copy):

  1. 一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。
  2. 深拷贝不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。
  3. 也就是说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间,所以深拷贝相比于浅拷贝速度较慢并且花销较大。

上面例子中邮件附件List attachments就是一个引用类型的成员变量,而jdk默认的clone方法就是浅拷贝实现。所以会导致张三和李四持有的其实是同一份附件列表数据,修改了张三的附件列表,就会导致李四的附件列表同时改变。

深拷贝实现

  1. 对象图中所有对象均重写clone()方法来实现深拷贝:对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。
  2. 通过对象序列化实现深拷贝

我们以序列化方式修改Email类的clone方法,同时让Email类实现java.io.Serializable接口让Email类支持序列化操作

/**
 * 邮件信息
 *
 * @author ccheng
 * @date 2021-12-18
 */
@Data
public class Email implements Cloneable, Serializable {
    /**
     * 发件人
     */
    private String fromUser;
    /**
     * 收件人
     */
    private String toUser;
    /**
     * 邮件主题
     */
    private String subject;
    /**
     * 邮件正文
     */
    private String context;
    /**
     * 邮件附件
     */
    private List<String> attachments;

    @Override
    public Email clone() {
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            //通过序列化方式实现深拷贝
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            return (Email) ois.readObject();
        } catch (Exception e) {
            throw new AssertionError();
        } finally {
            IoUtil.close(oos);
            IoUtil.close(ois);
        }
    }
}

我们再试试给张三发送一张“羞羞的图片”

/**
 * 原型模式-使用原型
 *
 * @author ccheng
 * @date 2021/12/18
 */
@Slf4j
public class Client {
    public static void main(String[] args) {
        String subject = "今晚打老虎";
        String context = "三缺一,速来";
        String fromUser="ccheng.top@qq.com";
        //邮件信息模板(原型实例)
        Email template = new Email();
        template.setSubject(subject);
        template.setContext(context);
        template.setFromUser(fromUser);
        //添加几张正经的图片
        List<String> attachments = new LinkedList<>();
        attachments.add("正经的图片1");
        attachments.add("正经的图片2");
        template.setAttachments(attachments);
        //发给张三的邮件
        Email email1 = template.clone();//通过clone方法得到新的邮件实例
        email1.setToUser("zhangsan@qq.com");
        //给张三追加一张羞羞的图片,李四没份
        email1.getAttachments().add("羞羞的图片");
        //发给李四的邮件
        Email email2 = template.clone();//通过clone方法得到新的邮件实例
        email2.setToUser("lisi@qq.com");

        //群发邮件
        List<Email> emails = new LinkedList<>();
        emails.add(email1);
        emails.add(email2);
        send(emails);
    }

    /**
     * 群发邮件
     */
    public static void send(List<Email> emails) {
        for (Email email : emails) {
            log.info("发送邮件:{}", email);
        }
    }
}

//运行结果
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=zhangsan@qq.com, subject=今晚打老虎, context=三缺一,速来, attachments=[正经的图片1, 正经的图片2, 羞羞的图片])
发送邮件:Email(fromUser=ccheng.top@qq.com, toUser=lisi@qq.com, subject=今晚打老虎, context=三缺一,速来, attachments=[正经的图片1, 正经的图片2])
    

从运行结果可以看到,“羞羞的图片”只发给了张三,李四没有看到,目的达成!

4、克隆破坏单例模式

如果我们克隆的目标的对象是单例对象,那么意味着,深克隆就会破坏单例。
我们可以通过以下方式防止深克隆破坏单例:

  1. 单例类不实现Cloneable接口
  2. 重写clone方法,在clone方法中返回单例对象即可

5、jdk中的原型模式

java.util.ArrayList就复写了clone方法
image.png

posted on 2021-12-19 00:41  _ccheng  阅读(34)  评论(0编辑  收藏  举报