设计模式 | 原型模式及典型应用

前言

本文的主要内容以下:php

  • 介绍原型模式css

  • 示例java

    • Java语言的cloneapache

    • 浅克隆与深克隆编程

    • 实现深克隆设计模式

  • 原型模式的典型应用数组

原型模式

原型模式(Prototype Pattern):使用原型实例指定建立对象的种类,而且经过拷贝这些原型建立新的对象。原型模式是一种对象建立型模式。微信

原型模式的工做原理很简单:将一个原型对象传给那个要发动建立的对象,这个要发动建立的对象经过请求原型对象拷贝本身来实现建立过程。网络

原型模式是一种“另类”的建立型模式,建立克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。mybatis

须要注意的是经过克隆方法所建立的对象是全新的对象,它们在内存中拥有新的地址,一般对克隆所产生的对象进行修改对原型对象不会形成任何影响,每个克隆对象都是相互独立的。经过不一样的方式修改能够获得一系列类似但不彻底相同的对象。

角色

  • Prototype(抽象原型类):它是声明克隆方法的接口,是全部具体原型类的公共父类,能够是抽象类也能够是接口,甚至还能够是具体实现类。

  • ConcretePrototype(具体原型类):它实如今抽象原型类中声明的克隆方法,在克隆方法中返回本身的一个克隆对象。

  • Client(客户类):让一个原型对象克隆自身从而建立一个新的对象,在客户类中只须要直接实例化或经过工厂方法等方式建立一个原型对象,再经过调用该对象的克隆方法便可获得多个相同的对象。因为客户类针对抽象原型类Prototype编程,所以用户能够根据须要选择具体原型类,系统具备较好的可扩展性,增长或更换具体原型类都很方便。

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

示例

Java语言提供的clone()方法

学过Java语言的人都知道,全部的Java类都继承自 java.lang.Object。事实上,Object 类提供一个 clone() 方法,能够将一个Java对象复制一份。所以在Java中能够直接使用 Object 提供的 clone() 方法来实现对象的克隆,Java语言中的原型模式实现很简单。

须要注意的是可以实现克隆的Java类必须实现一个 标识接口 Cloneable,表示这个Java类支持被复制。若是一个类没有实现这个接口可是调用了clone()方法,Java编译器将抛出一个 CloneNotSupportedException 异常。

public class Mail implements Cloneable{
    private String name;
    private String emailAddress;
    private String content;
    public Mail(){
        System.out.println("Mail Class Constructor");
    }
    // ...省略 getter、setter
    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("clone mail object");
        return super.clone();
    }
}

在客户端建立原型对象和克隆对象也很简单,以下代码所示:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Mail mail = new Mail();
        mail.setContent("初始化模板");
        System.out.println("初始化mail:"+mail);
        for(int i = 0;i < 3;i++){
            System.out.println();
            Mail mailTemp = (Mail) mail.clone();
            mailTemp.setName("姓名"+i);
            mailTemp.setEmailAddress("姓名"+i+"@test.com");
            mailTemp.setContent("恭喜您,这次抽奖活动中奖了");
            MailUtil.sendMail(mailTemp);
            System.out.println("克隆的mailTemp:"+mailTemp);
        }
        MailUtil.saveOriginMailRecord(mail);
    }
}

其中的 MailUtil 工具类为

public class MailUtil {
    public static void sendMail(Mail mail{
        String outputContent = "向{0}同窗,邮件地址:{1},邮件内容:{2}发送邮件成功";
        System.out.println(MessageFormat.format(outputContent, mail.getName(), mail.getEmailAddress(), mail.getContent()));
    }

    public static void saveOriginMailRecord(Mail mail{
        System.out.println("存储originMail记录,originMail:" + mail.getContent());
    }
}

输出以下:

Mail Class Constructor
初始化mail:Mail{name='null', emailAddress='null', content='初始化模板'}com.designpattern.prototype.Mail@12edcd21

clone mail object
向姓名0同窗,邮件地址:姓名0@test.com,邮件内容:恭喜您,这次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名0', emailAddress='姓名0@test.com', content='恭喜您,这次抽奖活动中奖了'}com.designpattern.prototype.Mail@34c45dca

clone mail object
向姓名1同窗,邮件地址:姓名1@test.com,邮件内容:恭喜您,这次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名1', emailAddress='姓名1@test.com', content='恭喜您,这次抽奖活动中奖了'}com.designpattern.prototype.Mail@52cc8049

clone mail object
向姓名2同窗,邮件地址:姓名2@test.com,邮件内容:恭喜您,这次抽奖活动中奖了发送邮件成功
克隆的mailTemp:Mail{name='姓名2', emailAddress='姓名2@test.com', content='恭喜您,这次抽奖活动中奖了'}com.designpattern.prototype.Mail@5b6f7412
存储originMail记录,originMail:初始化模板

从输出结果中咱们能够观察到:

  • for循环中的 mailTemp 从 mail 对象中克隆获得,它们的内存地址均不一样,说明不是同一个对象,克隆成功,克隆仅仅经过调用 super.clone() 便可。

  • 最后调用的 MailUtil.saveOriginMailRecord(mail); 中的 mail 对象的内容仍为 for 循环以前设置的内容,并无由于克隆而改变。

  • 克隆的时候调用了 clone 方法,并无调用 Mail 类的构造器,只在最前面 new 的时候才调用了一次

关于输出的内存地址是怎么输出的,咱们还须要看一下 Object#toString 方法

public class Object {
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    //...省略...
}

因此所谓的内存地址即为 hashCode() 的十六进制表示,这里简单的认为 内存地址相同则为同一个对象,不一样则为不一样对象

再来看一眼 Object#clone 方法

protected native Object clone() throws CloneNotSupportedException;

这是一个 native 关键字修饰的方法

通常而言,Java语言中的clone()方法知足:

  • 对任何对象x,都有 x.clone() != x,即克隆对象与原型对象不是同一个对象;

  • 对任何对象x,都有 x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型同样;

  • 若是对象x的 equals() 方法定义恰当,那么 x.clone().equals(x) 应该成立。

为了获取对象的一份拷贝,咱们能够直接利用Object类的clone()方法,具体步骤以下:

  1. 在派生类中覆盖基类的 clone() 方法,并声明为public;

  2. 在派生类的 clone() 方法中,调用 super.clone()

  3. 派生类需实现Cloneable接口。

此时,Object类至关于抽象原型类,全部实现了Cloneable接口的类至关于具体原型类

浅克隆与深克隆

看下面的示例

public class Pig implements Cloneable{
    private String name;
    private Date birthday;
    // ...getter, setter, construct
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig pig = (Pig)super.clone();
        return pig;
    }
    @Override
    public String toString() {
        return "Pig{" +
                "name='" + name + '\'' +
                ", birthday=" + birthday +
                '}'+super.toString();
    }
}

测试

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Date birthday = new Date(0L);
        Pig pig1 = new Pig("佩奇",birthday);
        Pig pig2 = (Pig) pig1.clone();
        System.out.println(pig1);
        System.out.println(pig2);

        pig1.getBirthday().setTime(666666666666L);

        System.out.println(pig1);
        System.out.println(pig2);
    }
}

输出以下

Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Thu Jan 01 08:00:00 CST 1970}com.designpattern.clone.Pig@312b1dae
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@27973e9b
Pig{name='佩奇', birthday=Sat Feb 16 09:11:06 CST 1991}com.designpattern.clone.Pig@312b1dae

咱们照着上一小节说的实现 Cloneable,调用 super.clone(); 进行克隆,中间咱们对 pig1 对象设置了一个时间戳,从输出中咱们能够发现什么问题呢?

咱们能够发现:

  • pig1pig2 的内存地址不一样

  • pig1 设置了时间,同事 pig2 的时间也改变了

咱们经过 debug 来看一下

debug查看对象地址

发现以下:

  • pig1 与 pig2 地址不同

  • pig1 的 birthday 与 pig2 的 birthday 同样

这里引出浅拷贝与深拷贝。

在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。

浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对二者进行详细介绍。

浅克隆:

  • 在浅克隆中,若是原型对象的成员变量是值类型,将复制一份给克隆对象;若是原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

  • 简单来讲,在浅克隆中,当对象被复制时只复制它自己和其中包含的值类型的成员变量,而引用类型的成员对象并无复制

  • 在Java语言中,经过覆盖Object类的clone()方法能够实现浅克隆。

深克隆:

  • 在深克隆中,不管原型对象的成员变量是值类型仍是引用类型,都将复制一份给克隆对象,深克隆将原型对象的全部引用对象也复制一份给克隆对象。

  • 简单来讲,在深克隆中,除了对象自己被复制外,对象所包含的全部成员变量也将复制。

  • 在Java语言中,若是须要实现深克隆,能够经过序列化(Serialization)等方式来实现。须要注意的是可以实现序列化的对象其类必须实现Serializable接口,不然没法实现序列化操做。

实现深克隆

方式一,手动对引用对象进行克隆:

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Pig pig = (Pig)super.clone();

        //深克隆
        pig.birthday = (Date) pig.birthday.clone();
        return pig;
    }

方式二,经过序列化的方式:

public class Pig implements Serializable {
    private String name;
    private Date birthday;
    // ...省略 getter, setter等

    protected Object deepClone() throws CloneNotSupportedException, IOException, ClassNotFoundException {
        //将对象写入流中
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bao);
        oos.writeObject(this);

        //将对象从流中取出
        ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return (ois.readObject());
    }
}
序列化方式的深克隆结果

破坏单例模式

饿汉式单例模式以下:

public class HungrySingleton implements SerializableCloneable {

    private final static HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {
        if (hungrySingleton != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    private Object readResolve() {
        return hungrySingleton;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

使用反射获取对象,测试以下

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        Method method = hungrySingleton.getClass().getDeclaredMethod("clone");
        method.setAccessible(true);
        HungrySingleton cloneHungrySingleton = (HungrySingleton) method.invoke(hungrySingleton);
        System.out.println(hungrySingleton);
        System.out.println(cloneHungrySingleton);
    }
}

输出

com.designpattern.HungrySingleton@34c45dca
com.designpattern.HungrySingleton@52cc8049

能够看到,经过原型模式,咱们把单例模式给破坏了,如今有两个对象了

为了防止单例模式被破坏,咱们能够:不实现 Cloneable 接口;或者把 clone 方法改成以下

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

原型模式的典型应用

  1. Object 类中的 clone 接口

  2. Cloneable 接口的实现类,能够看到至少一千多个,找几个例子譬如:

Cloneable接口的实现类

ArrayListclone 的重写以下:

public class ArrayList<Eextends AbstractList<E>
        implements List<E>, RandomAccessCloneablejava.io.Serializable 
{
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
    //...省略
}

调用 super.clone(); 以后把 elementData 数据 copy 了一份

同理,咱们看看 HashMapclone 方法的重写:

public class HashMap<K,Vextends AbstractMap<K,Vimplements Map<K,V>, CloneableSerializable {
    @Override
    public Object clone() {
        HashMap<K,V> result;
        try {
            result = (HashMap<K,V>)super.clone();
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
        result.reinitialize();
        result.putMapEntries(thisfalse);
        return result;
    }
    // ...省略...
}

mybatis 中的 org.apache.ibatis.cache.CacheKeyclone 方法的重写:

public class CacheKey implements CloneableSerializable {
    private List<Object> updateList;
    public CacheKey clone() throws CloneNotSupportedException {
        CacheKey clonedCacheKey = (CacheKey)super.clone();
        clonedCacheKey.updateList = new ArrayList(this.updateList);
        return clonedCacheKey;
    }
    // ... 省略...
}

这里又要注意,updateListList<Object> 类型,因此多是值类型的List,也多是引用类型的List,克隆的结果须要注意是否为深克隆或者浅克隆

使用原始模式的时候必定要注意为深克隆仍是浅克隆。

原型模式总结

原型模式的主要优势以下:

  • 当建立新的对象实例较为复杂时,使用原型模式能够简化对象的建立过程,经过复制一个已有实例能够提升新实例的建立效率。

  • 扩展性较好,因为在原型模式中提供了抽象原型类,在客户端能够针对抽象原型类进行编程,而将具体原型类写在配置文件中,增长或减小产品类对原有系统都没有任何影响。

  • 原型模式提供了简化的建立结构,工厂方法模式经常须要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不须要这样,原型模式中产品的复制是经过封装在原型类中的克隆方法实现的,无须专门的工厂类来建立产品。

  • 可使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在须要的时候使用(如恢复到某一历史状态),可辅助实现撤销操做。

原型模式的主要缺点以下:

  • 须要为每个类配备一个克隆方法,并且该克隆方法位于一个类的内部,当对已有的类进行改造时,须要修改源代码,违背了“开闭原则”。

  • 在实现深克隆时须要编写较为复杂的代码,并且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

适用场景:

  • 建立新对象成本较大(如初始化须要占用较长的时间,占用太多的CPU资源或网络资源),新的对象能够经过原型模式对已有对象进行复制来得到,若是是类似对象,则能够对其成员变量稍做修改。

  • 若是系统要保存对象的状态,而对象的状态变化很小,或者对象自己占用内存较少时,可使用原型模式配合备忘录模式来实现。

  • 须要避免使用分层次的工厂类来建立分层次的对象,而且类的实例对象只有一个或不多的几个组合状态,经过复制原型对象获得新实例可能比使用构造函数建立一个新实例更加方便。


参考:  
刘伟:设计模式Java版  
慕课网java设计模式精讲 Debug 方式+内存分析

推荐阅读

设计模式 | 简单工厂模式及典型应用  
设计模式 | 工厂方法模式及典型应用    
设计模式 | 抽象工厂模式及典型应用    
设计模式 | 建造者模式及典型应用

关注_小旋锋_微信公众号


本文分享自微信公众号 - 小旋锋(whirlysBigData)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索