设计模式

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

2019-01-03  本文已影响1人  小旋锋的简书

前言

本文的主要内容如下:

原型模式

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

原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。

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

需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。

角色

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

示例

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:初始化模板

从输出结果中我们可以观察到:

关于输出的内存地址是怎么输出的,我们还需要看一下 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()方法满足:

为了获取对象的一份拷贝,我们可以直接利用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 对象设置了一个时间戳,从输出中我们可以发现什么问题呢?

我们可以发现:

我们通过 debug 来看一下

debug查看对象地址

发现如下:

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

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

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

浅克隆:

深克隆:

实现深克隆

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

    @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 Serializable, Cloneable {

    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<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.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,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    @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(this, false);
        return result;
    }
    // ...省略...
}

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

public class CacheKey implements Cloneable, Serializable {
    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,克隆的结果需要注意是否为深克隆或者浅克隆

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

原型模式总结

原型模式的主要优点如下:

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

适用场景:

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

后记

欢迎评论、转发、分享,您的支持是我最大的动力

更多内容可访问我的个人博客:http://laijianfeng.org

关注【小旋锋】微信公众号,及时接收博文推送

关注_小旋锋_微信公众号
上一篇 下一篇

猜你喜欢

热点阅读