首页 > 技术文章 > [置顶] 设计模式(四)原型模式

jeofey 2015-08-21 14:14 原文

概念

原型模式是指用原型实例指定创建对象的种类,并通过拷贝这些原型来创建新的实例。也就是说原型模式是通过复制现在已存在的对象来创建一个新的对象,被拷贝的对象和新创建的对象类型相同(是同一个类的实例)。使用原型模式时,我们首先要创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象。原型模式是一种对象创建型模式,创建拷贝对象的工厂是原型类本省,工厂方法由拷贝方法来实现。原型模式的核心是实现拷贝方法。

java中实现拷贝的两种常用方法

原型模式的核心是实现Clonable接口,重写clone方法。

通用实现方法

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

public class ConcretePrototype{
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    /**
     * 拷贝方法
     * @return
     */
    public ConcretePrototype clone(){
        ConcretePrototype prototype = new ConcretePrototype();//创建新对象
        prototype.setUserName(this.userName);
        return prototype;
    }
}

class Client{
    public static void main(String[] args){
        ConcretePrototype obj1 = new ConcretePrototype();
        obj1.setUserName("我是小白");
        ConcretePrototype obj2 = obj1.clone();
        System.out.println(obj1.getUserName()+":"+obj2.getUserName());
    }
}

运行结果

我是小白:我是小白

java语言提供的clone()方法

在java语言中所有的java类都继承自java.lang.Object。而Object类提供了一个clone()方法,可以将一个java对象复制一份。注意:需要实现拷贝的类必须实现Cloneable标识接口,否则当该类调用了clone()方法,编译器会报异常CloneNotSupportException。

class Client{
    public static void main(String[] args){
        Prototype prototype = new Prototype();
        prototype.setUserName("我是小黑");
        Prototype prototype1 = prototype.clone();
        System.out.println(prototype.getUserName()+":"+prototype1.getUserName());
    }
}

class Prototype implements Cloneable{
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Prototype clone(){
        Object obj = null;
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return (Prototype)obj;
    }
}
运行结果

我是小黑:我是小黑

解析:java中所有普通类都继承Object类,既然Object类有clone方法,为什么普通了需要实现拷贝的话,还要重写clone()方法?看Object类源码就会发现,该方法的定义是:protected native Object clone() throws CloneNotSupportedException;即protected关键字修饰的方法,在同一个包下的类才能调用,而普通类和Object不在同一个包下,所以不能直接调用。

如果不实现Cloneable接口,运行该示例,就会报异常

java.lang.CloneNotSupportedException: com.xianjj.patterns.prototype.Prototype
	at java.lang.Object.clone(Native Method)
	at com.xianjj.patterns.prototype.Prototype.clone(ConcretePrototype.java:59)
	at com.xianjj.patterns.prototype.Client.main(ConcretePrototype.java:40)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Exception in thread "main" java.lang.NullPointerException
	at com.xianjj.patterns.prototype.Client.main(ConcretePrototype.java:41)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

浅拷贝与深拷贝

浅拷贝只复制当前对象的变量,不拷贝它所引用的对象,所有的对其他对象的引用仍然指向原来的对象;深拷贝把当前对象和它所引用的对象都拷贝一遍,拷贝前后两个对象相对独立互不影响。

浅拷贝示例

public class User implements Cloneable{

    private String userId;
    private String userName;

    @Override
    public User clone(){
        User user = null;
        try {
            user = (User) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return user;
    }
//省略get、set方法。。。
}
public class CloneTest implements Cloneable{
    private User user = new User();
    private int status;

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

    public void getValue() {
        System.out.println("原始类型int:"+status+";引用类型User的属性userName:"+user.getUserName());
    }

    public void setValue(int status, String userId, String userName) {
        this.status = status;
        this.user.setUserId(userId);
        this.user.setUserName(userName);
    }

}

class CloneClient{
    public static void main(String[] args){
        CloneTest obj1 = new CloneTest();
        obj1.setValue(1, "1", "小黑");

        CloneTest obj2 = obj1.clone();
        obj2.setValue(2, "2", "小白");

        obj1.getValue();
        obj2.getValue();
    }
}
输出结果

原始类型int:1;引用类型User的属性userName:小白
原始类型int:2;引用类型User的属性userName:小白

结果分析:
拷贝后的对象重新赋值时,两个对象原始类型int类变量status的值不一样,而应用类型User的值一样,即拷贝对象obj2中的user值覆盖了原型对象obj1中user的值。其根本原因是Object类提供的clone()方法只拷贝本对象,对其内部的数组、应用对象都不拷贝,还是指向原生对象的内部元素地址,两个对象共享了一个私有变量,你改我改大家都能改,这是一种非常不安全的方式。其他的原始类型(int,long,包含String)都会被拷贝。

实现深拷贝

在上述拷贝方法中添加一行代码,即自行拷贝User对象

@Override
    public CloneTest clone(){
        CloneTest object = null;
        try {
            object = (CloneTest) super.clone();
            object.user = this.user.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return object;
    }
输出结果:
原始类型int:1;引用类型User的属性userName:小黑
原始类型int:2;引用类型User的属性userName:小白

在java中如果要实现深拷贝,可以通过序列化方式(Serialization)来实现。序列化是将对象写到流的过程,写到流的对象是原有对象的一个拷贝,然原始对象仍然存与内从中。通过序列化拷贝不仅可以复制对象本身,而且可以复制其应用的成员对象,因此通过序列化将一个对象写到流中国,再从流里读出来可以实现深拷贝。能够实现序列化的类必须实现Serialization接口。注意:Java中== 比较的是两个对象的地址

示例展示

示例背景:一些电商为了维护老用户,会隔一段时间,通过短信或邮件的方式给用户发送优惠券或者大额折扣等优惠信息,刺激用户消费;短信内容基本一致,
只有抬头信息,收件人不同,假设老用户有600万,依次循环发送600万次,在循环中重新设定短信内容的抬头信息和收件人,用单线程能实现功能,但是耗时比较长,
如果用多线程,第一个线程没发送完,新启的线程会改动发送内容,这样线程及其不安全;那么有没有更好的方式来实现呢?用原型模式。

public class Msg implements Cloneable{
    private String receiver;//收件人
    private String appellation;//称谓
    private String subject;
    private String context;

    public Msg(MsgTemplete msgTemplete) {
        this.context = msgTemplete.getContext();
        this.subject = msgTemplete.getSubject();
    }

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

    //省略get、set方法
}

class MsgTemplete{
    private String subject="XX店铺会员送优惠券活动";
    private String context = "老会员只要下单即送价值1万元的优惠券,可低现金使用。。。";
    public String getSubject(){
        return this.subject;
    }
    public String getContext(){
        return this.context;
    }
}

class MsgClient{
    private static int SEND_MAX = 5;//发送短信的数量

    public static void sendMsg(Msg msg){
        System.out.println("收件人:"+msg.getReceiver()+";短信主题:"+msg.getSubject()+";短信内容:"+msg.getAppellation()+msg.getContext());
    }
    public static void main(String[] args){
        int i = 0;
        //定义模板,可以从数据库读取
        Msg msg = new Msg(new MsgTemplete());
        while (i < SEND_MAX){
            //单独设定每条短信的特殊内容
            Msg msgClone = msg.clone();
            msgClone.setAppellation(getRandString(5)+"先生(女士)");
            msgClone.setReceiver(getRandString(5)+"@"+getRandString(3)+".com");
            //发送短信
            sendMsg(msgClone);
            i++;
        }
    }

    //获取指定长度的字符串模式收件人
    public static String getRandString(int maxLength){
        String source = "abcdefghijiklmnopqrstuvwrstABCDEFGHIJKLMNOPQRSTUVWXYZ";
        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();
    }
}
输出结果:

收件人:HGEJX@ijn.com;短信主题:XX店铺会员送优惠券活动;短信内容:JVJTZ先生(女士)老会员只要下单即送价值1万元的优惠券,可低现金使用。。。
收件人:ZUTsf@FPg.com;短信主题:XX店铺会员送优惠券活动;短信内容:tRKfg先生(女士)老会员只要下单即送价值1万元的优惠券,可低现金使用。。。
收件人:vpfcq@cvW.com;短信主题:XX店铺会员送优惠券活动;短信内容:wqCSS先生(女士)老会员只要下单即送价值1万元的优惠券,可低现金使用。。。
收件人:SpwLd@Ltp.com;短信主题:XX店铺会员送优惠券活动;短信内容:OeFNf先生(女士)老会员只要下单即送价值1万元的优惠券,可低现金使用。。。
收件人:rYZVF@sAJ.com;短信主题:XX店铺会员送优惠券活动;短信内容:Wnibn先生(女士)老会员只要下单即送价值1万元的优惠券,可低现金使用。。。

这里如果是多线程执行发送短信,也不会出现线程安全问题。

原型模式优缺点

  • 性能优良:原型模式是内存二进制流的拷贝,要不new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以很好的提现其优点。
  • 逃避构造函数的约束:直接在内存(堆内存)中拷贝构造函数是不会执行的。这既是有点也是缺点。

实际项目中原型模式很少单独出现,一般是与工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。





推荐阅读