设计模式之四——原型模式

1.原型模式简介

1.1定义

用原型实例指定将要创建的对象类型,并且通过复制这个实例创建新的对象。总得来说,原型模式实际上就是从一个对象创建另一个新的对象,使新的对象有具有原对象的特征.
克隆模式类似于new 但是不同于new,new创建新的对象属性采用的是默认值,克隆出的对象的属性完全与原型对象相同,并且克隆出的新对象改变不会影响原型对象,然后在修改克隆对象的值.

1.2特点

  • 必须存在一个现有的对象,也就是原型实例,通过原型实例创建新对象。
  • 在Java中,实现Cloneable,并且因为所有的类都继承Object类,重写clone()方法来实现拷贝。

1.3角色

  • Prototype(抽象原型类):它是所有具体原型类的公共父类,定义了的具体原型类所需的实现的方法。可以是抽象类也可以是接口,甚至还可以是具体实现类。
  • ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  • Client(客户类):提出创建对象的请求,让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
    在这里插入图片描述

1.4模式分析

在原型模式结构中定义了一个抽象原型类,所有的Java类都继承自java.lang.Object,而Object类提供一个clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。

能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。

注意: ```java.lang.Cloneable`` 只是起到告诉程序可以调用clone方法的作用,它本身并没有定义任何方法。

在使用原型模式克隆对象时,根据其成员对象是否也克隆,原型模式可以分为两种形式:深克隆 和 浅克隆 。

1.5原型模式适用场景

我们现在一般会使用new关键字指定类名生成类的实例(PS:我们以前使用java.lang.Cloneable的一个很大原因是使用new创建对象的速度相对来说会慢一些,随着JVM性能的提升,new的速度和Object的clone()方法的速度差不多了。)。
使用new关键字创建类的时候必须指定类名,但是在开发过程中也会有“在不指定类名的前提下生成实例”的需求。例如,在下面这些情况下,就需要根据现有的实例来生成新的实例。

  1. 对象种类繁多,无法将他们整合到一个类的时候;
  2. 难以根据类生成实例时;
  3. 想解耦框架与生成的实例时。
    如果想要让生成实例的框架不再依赖于具体的类,这时,不能指定类名来生成实例,而要事先“注册”一个“原型”实例,然后通过复制该实例来生成新的实例。

1.6原型模式的实例的拷

  1. 浅复制:将一个对象复制后,其基本数据类型的变量都会重新创建,而引用类型的变量指向的还是原对象所指向的,也就是指向的内存堆地址没变。
  2. 深复制:将一个对象复制后,不论是基本数据类型还是引用类型,都是重新创建的。

2.原型模式的实现

具体原型角色代码如下:

public class Money implements Cloneable {

    private int faceValue;

    private Area area;

    public int getFaceValue() {
        return faceValue;
    }

    public void setFaceValue(int faceValue) {
        this.faceValue = faceValue;
    }

    public Money(int faceValue, Area area) {
        this.faceValue = faceValue;
        this.area = area;
    }

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    @Override
    protected Money clone() throws CloneNotSupportedException {
        return (Money) super.clone();
    }
}

Area类代码如下:

public class Area {

    // 钞票单位
    private String unit;

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

}

客户端实现钞票的拷贝

public class Client {

    public static void main(String[] args) {

        Area area = new Area();
        area.setUnit("RMB");

        // 原型实例,100RMB的钞票
        Money money = new Money(100, area);

        for (int i = 1; i <= 3; i++) {
            try {
                Money cloneMoney = money.clone();
                cloneMoney.setFaceValue(i * 100);
                System.out.println("这张是" + cloneMoney.getFaceValue() +  cloneMoney.getArea().getUnit() + "的钞票");
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:

这张是100RMB的钞票
这张是200RMB的钞票
这张是300RMB的钞票

从上面并没有看到抽象原型角色的代码,那该角色在哪?Object就是这个抽象原型角色,因为Java中所有的类都默认继承Objet,在这提供clone方法。

2.1浅拷贝

改变客户端代码:

public class Client {

    public static void main(String[] args) {

        Area area = new Area();
        area.setUnit("RMB");
        // 原型实例,100RMB的钞票
        Money money = new Money(100, area);
        try {
            Money cloneMoney = money.clone();
            cloneMoney.setFaceValue(200);
            area.setUnit("美元"); 

            System.out.println("原型实例的面值:" + money.getFaceValue() +money.getArea().getUnit());
            System.out.println("拷贝实例的面值:" + cloneMoney.getFaceValue() + cloneMoney.getArea().getUnit());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

}

运行结果:

原型实例的面值:100美元
拷贝实例的面值:200美元

我们只是把原型实例的单位改成了美元而已,拷贝实例为什么也会跟着改变的?因为我们用的是Object的clone方法,而该方法只拷贝按值传递的数据,比如String类型和基本类型,但对象内的数组、引用对象都不拷贝,也就是说内存中原型实例和拷贝实例指向同一个引用对象的地址,这就是浅拷贝。浅拷贝的内存变化如下图:

在这里插入图片描述
从上图可以看出,浅拷贝前后的两个实例对象共同指向同一个内存地址,即它们共有拥有area1实例,同时也存在着数据被修改的风险。注意,这里不可拷贝的引用对象是指可变的类成员变量。

2.2深拷贝

实现深拷贝就需要完全的拷贝,包括引用对象,数组的拷贝。所以Area类也实现了Cloneable接口,重写了clone方法,代码如下:

public class Area implements Cloneable{

    // 钞票单位
    private String unit;

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    @Override
    protected Area clone() throws CloneNotSupportedException {
        Area cloneArea = (Area) super.clone();
        return cloneArea;
    }
}

在Money钞票类的clone方法增加拷贝Area的代码:

public class Money implements Cloneable, Serializable {

    private int faceValue;

    private Area area;

    public int getFaceValue() {
        return faceValue;
    }

    public void setFaceValue(int faceValue) {
        this.faceValue = faceValue;
    }

    public Money(int faceValue, Area area) {
        this.faceValue = faceValue;
        this.area = area;
    }

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }

    @Override
    protected Money clone() throws CloneNotSupportedException {
        Money cloneMoney = (Money) super.clone();
        cloneMoney.area = this.area.clone();  // 增加Area的拷贝
        return cloneMoney;
    }

}

修改客户端代码如下:

public class Client {

    public static void main(String[] args) {

        Area area = new Area();
        area.setUnit("RMB");

        // 原型实例,100RMB的钞票
        Money money = new Money(100, area);

        try {
            Money cloneMoney = money.clone();
            cloneMoney.setFaceValue(200);
            area.setUnit("美元");

            System.out.println("原型实例的面值:" + money.getFaceValue() + money.getArea().getUnit());
            System.out.println("拷贝实例的面值:" + cloneMoney.getFaceValue() + cloneMoney.getArea().getUnit());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

原型实例的面值:100美元
拷贝实例的面值:200RMB

深拷贝除了需要拷贝值传递的数据,还需要拷贝引用对象、数组,即把所有引用的对象都拷贝。需要注意的是拷贝的引用对象是否还有可变的类成员对象,如果有就继续对该成员对象进行拷贝,如此类推。所以使用深拷贝是注意分析拷贝有多深,以免影响性能。深拷贝的内存变化如下图:
在这里插入图片描述

2.3序列化实现深拷贝

这是实现深拷贝的另一种方式,通过二进制流操作对象,从而达到深拷贝的效果。把对象写到流里的过程是序列化过程,而把对象从流中读出来的过程则叫反序列化过程。深拷贝的过程就是把对象序列化(写成二进制流),然后再反序列化(从流里读出来)。注意,在Java中,常常可以先使对象实现Serializable接口,包括引用对象也要实现Serializable接口,不然会抛NotSerializableException。

只修改Money,代码如下:

public class Money implements Serializable {

    private int faceValue;

    private Area area;

    public int getFaceValue() {
        return faceValue;
    }

    public void setFaceValue(int faceValue) {
        this.faceValue = faceValue;
    }

    public Money(int faceValue, Area area) {
        this.faceValue = faceValue;
        this.area = area;
    }

    public Area getArea() {
        return area;
    }

    public void setArea(Area area) {
        this.area = area;
    }

    @Override
    protected Money clone() throws CloneNotSupportedException {
        Money money = null;
        try {
            // 调用deepClone,而不是Object的clone方法
            cloneMoney = (Money) deepClone();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return cloneMoney;
    }

    // 通过序列化深拷贝
    public Object deepClone() throws IOException, ClassNotFoundException {
        //将对象写到流里
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        //从流里读回来
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
}

运行结果:

原型实例的面值:100美元
拷贝实例的面值:200RMB

3.优缺点

原型模式的优点:

  • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过一个已有实例可以提高新实例的创建效率。
  • 可以动态增加或减少产品类。
  • 原型模式提供了简化的创建结构。
  • 可以使用深克隆的方式保存对象的状态。

原型模式的缺点:

  • 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
  • 在实现深克隆时需要编写较为复杂的代码。

4.实际应用案例

(1) 原型模式应用于很多软件中,如果每次创建一个对象要花大量时间,原型模式是最好的解决方案。很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的应用,复制得到的对象与原型对象是两个类型相同但内存地址不同的对象,通过原型模式可以大大提高对象的创建效率。

(2) 在Struts2中为了保证线程的安全性,Action对象的创建使用了原型模式,访问一个已经存在的````Action对象时将通过克隆的方式创建出一个新的对象,从而保证其中定义的变量无须进行加锁实现同步,每一个Action中都有自己的成员变量,避免Struts1```因使用单例模式而导致的并发和同步问题。

(3) 在Spring中,用户也可以采用原型模式来创建新的bean实例,从而实现每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值