设计模式-原型模式

原型模式介绍

原型模式(Prototype Pattern)是创建型模式的一种。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。

原型模式定义

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

原型模式使用场景

  1. 如果类的初始化需要耗费较多的资源,那么可以通过原型拷贝避免这些消耗。
  2. 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
  3. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以拷贝多个对象供调用者使用,即保护性拷贝。

原型模式 UML 类图


角色介绍:

  • Client:客户端角色。
  • Prototype:抽象原型角色,抽象类或者接口,用来声明clone方法。
  • ConcretePrototype:具体的原型类,是客户端角色使用的对象,即被复制的对象。

需要注意的是,Prototype 通常是不用自己定义的,因为拷贝这个操作十分常用,Java 中就提供了Cloneable 接口来支持拷贝操作,它就是原型模式中的Prototype。当然,原型模式也未必非得去实现Cloneable接口,也有其他的实现方式。

原型模式的实现

原型模式的核心是clone方法,通过该方法进行拷贝,这里举一个名片拷贝的例子。
现在已经流行电子名片了,只要扫一下就可以将名片拷贝到自己的名片库中, 我们先实现名片类。
Java 原型类就是 Cloneable 了

具体原型类

public class BusinessCard {
    private String name;
    private String company;

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

    public void setName(String name) {
        this.name = name;
    }

    public void setCompany(String company) {
        this.company = company;
    }

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

    public void show() {
        System.out.println("name:" + name + ", company:" + company);
    }
}

注意: Cloneable 是一个空接口,clone 方法是在 Object 类中定义,访问类型为 protected,当执行 clone 方法时会检测是不是 Cloneable 类型,如果不是就会报java.lang.CloneNotSupportedException 错误。

客户端

public class Client {
    public static void main(String[] args) {
        BusinessCard businessCard = new BusinessCard();
        businessCard.setName("小明");
        businessCard.setCompany("百度");
  
        BusinessCard cloneCard1 = businessCard.clone();
        cloneCard1.setName("小强");
        cloneCard1.setCompany("阿里");

        BusinessCard cloneCard2 = businessCard.clone();
        cloneCard2.setName("小红");
        cloneCard2.setCompany("腾讯");

        businessCard.show();
        cloneCard1.show();
        cloneCard2.show();
    }
}

除了第一个名片,其他两个名片都是通过clone方法得到的,需要注意的是,clone方法返回的对象是在内存中二进制拷贝的原对象,并不会执行其构造方法。运行结果为:

执行构造函数BusinessCard
name:小明, company:百度
name:小强, company:阿里
name:小红, company:腾讯

浅拷贝和深拷贝

原型模式涉及到浅拷贝和深拷贝的知识点,为了更好的理解它们,还需要举一些例子。

实现浅拷贝
上述的例子中,BusinessCard 的字段都是String类型的,如果字段是引用的类型的,会出现什么情况呢?如下所示:

public class BusinessCard implements Cloneable {
    private String name;
    private Company company = new Company();

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

    public void setName(String name) {
        this.name = name;
    }

    public void setCompany(String name, String address) {
        company.name = name;
        company.address = address;
    }

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

    public void show() {
        System.out.println("name:" + name + ", company:" + company);
    }
}
public class Company {
    String name;
    String address;

    @Override
    public String toString() {
        return "Company{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

客户端代码

public class Client {
    public static void main(String[] args) {
        BusinessCard businessCard = new BusinessCard();
        businessCard.setName("小明");
        businessCard.setCompany("百度", "北京");

        BusinessCard cloneCard1 = businessCard.clone();
        cloneCard1.setName("小强");
        cloneCard1.setCompany("阿里", "杭州");

        BusinessCard cloneCard2 = businessCard.clone();
        cloneCard2.setName("小红");
        cloneCard2.setCompany("腾讯", "深圳");

        businessCard.show();
        cloneCard1.show();
        cloneCard2.show();
    }
}

执行结果:

执行构造函数BusinessCard
name:小明, company:Company{name='腾讯', address='深圳'}
name:小强, company:Company{name='腾讯', address='深圳'}
name:小红, company:Company{name='腾讯', address='深圳'}

从结果可以看出 company 字段为最后设置的”腾讯”、”深圳”。这是因为 Object 类提供的 clone 方法,不会执行对象中的内部数组和引用对象的 clone 方法,导致 clone 出的对象的 company 指向的对象仍旧是原型中的 company 指向的对象。这种拷贝叫做浅拷贝

浅拷贝显然不符号要求,实际开发时我们希望每个对象可以独立变化,此时我们就需要通过深拷贝来解决,也就是执行 clone 方法时,也需要执行内部对象的 clone 方法。

实现深拷贝
首先需要让 Company 支持 clone,修改代码如下:

public class Company implements Cloneable {
    String name;
    String address;

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

    @Override
    public String toString() {
        return "Company{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

修改 BusinessCard 的 clone 方法

public class BusinessCard implements Cloneable {
  ...
  @Override
    public BusinessCard clone() {
        BusinessCard businessCard = null;
        try {
            businessCard = (BusinessCard) super.clone();
            businessCard.company = company.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return businessCard;
    }
}

Client 不变,运行结果如下:

执行构造函数BusinessCard
name:小明, company:Company{name='百度', address='北京'}
name:小强, company:Company{name='阿里', address='杭州'}
name:小红, company:Company{name='腾讯', address='深圳'}

总结

原型模式比较简单,核心问题就是对原始对象进行拷贝,需要注意的就是深、浅拷贝的问题。开发中,为了避免操作副本时影响原始对象,应尽量采用深拷贝。

优点
原型模式是在内存中二进制流的拷贝,要比 new 一个对象的性能要好,特别是需要产生大量对象时。
缺点
直接在内存中拷贝,构造函数是不会执行的,这样就减少了约束,这既是优点也是缺点,需要在实际应用中去考量。

Android 源码中的原型模式

Android 源码中的常用的 Intent 就实现了 clone 方法。

public class Intent implements Parcelable, Cloneable {
    public Intent(Intent o) {
        this(o, COPY_MODE_ALL);
    }

    private Intent(Intent o, @CopyMode int copyMode) {
        this.mAction = o.mAction;
        this.mData = o.mData;
        this.mType = o.mType;
        this.mPackage = o.mPackage;
        this.mComponent = o.mComponent;

        if (o.mCategories != null) {
            this.mCategories = new ArraySet<>(o.mCategories);
        }

        if (copyMode != COPY_MODE_FILTER) {
            this.mFlags = o.mFlags;
            this.mContentUserHint = o.mContentUserHint;
            this.mLaunchToken = o.mLaunchToken;
            if (o.mSourceBounds != null) {
                this.mSourceBounds = new Rect(o.mSourceBounds);
            }
            if (o.mSelector != null) {
                this.mSelector = new Intent(o.mSelector);
            }

            if (copyMode != COPY_MODE_HISTORY) {
                if (o.mExtras != null) {
                    this.mExtras = new Bundle(o.mExtras);
                }
                if (o.mClipData != null) {
                    this.mClipData = new ClipData(o.mClipData);
                }
            } else {
                if (o.mExtras != null && !o.mExtras.maybeIsEmpty()) {
                    this.mExtras = Bundle.STRIPPED;
                }
            }
        }
    }    
    @Override
    public Object clone() {
        return new Intent(this);
    }
    ...
}

可以看到 clone 方法实际上内部没有调用 super.clone() 方法来实现对象拷贝,而是调用了 new Intent(this)。可以看到 Intent 中有很多参数,使用 clone 即可增加构建对象的便捷性,在 clone 中使用 new,可以实现定制化拷贝对象,如果使用 super.clone() 则会将原对象进行拷贝。
使用 new 和 clone 需要根据构造对象的成本来决定,如果对象的构造成本比较高或者比较麻烦,那么使用 clone 方法效率更高,否则就使用 new

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354