Java设计模式学习记录-原型模式

前言

最近一直在面试,也没时间写博客了,感觉已经积攒了好多知识想要记录下来了,因为在面试中遇到的没答出来的问题,这就是自己不足的地方,然后就要去学习这部分内容,虽然说自己不足的地方学习了,但是没有应用到具体实际的地方,过段时间也还是会忘,所以我的办法是用博客记录下来。

俗话说“好记性不如烂笔头”,在我这里是“好记性不如烂博客”😂。

今天要介绍的原型模式也是创建型模式中的一种,感觉叫复制方法模式或许更接地气一些,我的理解就是用一个对象复制出另一对象。例如《西游记》中孙悟空拔几根猴毛就能变出好几个一样的孙猴子来。其中孙悟空就是一个原型,创建孙猴子的过程就是实现原型模式的过程。

原型模式

原型模式介绍

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

在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象,来创建更多的同类型的对象。

如何实现复制

原型模式中到底是如何实现复制的呢?下面介绍两种实现方式。

1、通用的方式

通用的方式是在具体的原型类的复制方法中,实例化一个与自身类型一样的对象,传入相同的属性值,然后将其返回。

如下代码方式:

public class PrototypeTest {

    //属性变量
    private String name;

    public String getName() {
        return name;
    }

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

    /**
     * 复制方法
     * @return
     */
    protected PrototypeTest clone()  {

        PrototypeTest prototypeTest = new PrototypeTest();
        prototypeTest.setName(name);
        return prototypeTest;
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        PrototypeTest prototypeTest = new PrototypeTest();
        prototypeTest.setName("第三");
    //复制原型 PrototypeTest cloneObject
= prototypeTest.clone(); System.out.println(Objects.toString(prototypeTest)); System.out.println(Objects.toString(cloneObject)); } }

输出的结果是:

PrototypeTest(name=第三)
PrototypeTest(name=第三)

这种方式通用性很高,并且与编程语言特性无关,任何一种面向对象的语言都可以使用这种形式来实现对原型的复制。

2、Java中的Object的clone()方法

因为在Java中所有的Java类都继承自java.lang.Object。而Object的类中提供一个默认的clone()方法,可以将一个Java对象复制一份。因此在Java中可以直接使用Object提供的clone()方法来实现对象的复制,这样实现原型模式就比较简单了。

需要注意的是,能够调用clone()实现拷贝的Java类,必须实现一个标识接口Cloneable,表示这个Java类支持被复制,为什么说是标识接口呢,因为这个接口里面没有定义任何方法,只是用了标识可以执行某些操作。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedExecption异常。

如下代码方式:

public class PrototypeMain implements Cloneable{

    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 重写Object的clone方法
     * @return
     */
    @Override
    protected PrototypeMain clone() {
        PrototypeMain prototypeMain = null;
        try {
             prototypeMain = (PrototypeMain)super.clone();
        }catch (CloneNotSupportedException e){
            System.err.println("Not Support Cloneable");
        }
        return prototypeMain;
    }

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

    //测试
    public static void main(String[] args) {
        PrototypeMain prototypeMain = new PrototypeMain();
        prototypeMain.setName("小花");
        prototypeMain.setAge(19);
        PrototypeMain cloneObject = prototypeMain.clone();

        System.out.println(Objects.toString(cloneObject));
    }
}

运行结果:

PrototypeMain{name='小花', age=19}

此时Object类可以理解为抽象原型类,而实现了Cloneable接口的类相当于具体原型类。

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

原型模式的结构如下图:

在原型模式结构图中包含如下3个角色。

Prototype(抽象原型类):这是声明复制方法的接口,是所有具体原型类的公共父类,可以是抽象类,也可以是接口,甚至可以是实现类。在上面介绍的实现复制的第二种方法里面的java.lang.Object类就是担当的这个角色。

ConcretePrototype(具体原型类):实现抽象原型类中声明的复制方法,在复制方法中返回一个与自己同类的复制对象。在上面介绍的实现复制的第二种方法里面的PrototypeMain类就是担当的这个角色。

Client(客户类):让一个原型对象复制自身,从而创建一个新的的对象。在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的复制方法,就可以得到多个相同的对象了。在上面介绍的实现复制的第二种方法里面,我将main方法写在了具体原型类中,如果将main方法提出到一个新的的使用类中,那么这个使用类就是客户类。

深Copy与浅Copy

浅Copy是指被复制的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都指向原来的对象。简单点说就是,只复制了引用,而没有复制真正的对象内容。

深Copy是指被复制的对象的所有变量都含有与原来对象相同的值,属性中的对象都指向被复制过的新对象中属性,而不再是原型对象中的属性。简单点说,就是深Copy把所有的对象的引用以及对象都复制了一遍,在堆中是存在两个相互独立的对象,以及属性中的对象也是相互独立的。

我们还是举例来说明吧:

如下代码,创建一个原型类。

public class ShallowCopy implements Cloneable {
    //对象属性
    private ArrayList<String> nameList = new ArrayList<>();

    /**
     * 复制方法
     * @return
     */
    @Override
    protected ShallowCopy clone() {

        ShallowCopy shallowCopy = null;
        try{
            shallowCopy = (ShallowCopy)super.clone();

        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return shallowCopy;
    }

    /**
     * 获得属性
     * @return
     */
    public ArrayList<String> getNameList() {
        return nameList;
    }

    /**
     * 填充属性值
     * @param name
     */
    public void setNameList(String name) {
        this.nameList.add(name);
    }
}

在客户类种使用,进行复制。

public class ClientTest {

    @Test
    public void test(){

        //创建一个对象
        ShallowCopy shallowCopy = new ShallowCopy();
        shallowCopy.setNameList("小红");

        //复制一个新对象
        ShallowCopy newObject = shallowCopy.clone();
        //给新对象的属性赋值
        newObject.setNameList("大黄");

        System.out.println(shallowCopy.getNameList());
    }

}

预想的结果应该是:小红,实际输出:

[小红, 大黄]

产生这种结果的原因是因为Object类的clone()方法导致的,clone()方法在复制对象时,只是复制本对象的引用,对其内部的数组、引用对象等都不复制,还是指向原生对象的内部元素地址,这种复制方式就是浅Copy。在实际项目中使用这种方式的还是比较少的。一般内部的数组和引用对象才不复制,其他的原始类型int、long、double等类型是会被复制的。另外String类型也是会被复制的,String类里是没有clone()的。

那么如何实现深Copy呢?

将上面的复制方法的代码改造一下:

   /**
     * 复制方法
     * @return
     */
    @Override
    protected ShallowCopy clone() {

        ShallowCopy shallowCopy = null;
        try{
            shallowCopy = (ShallowCopy)super.clone();
            shallowCopy.nameList = (ArrayList<String>) this.nameList.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        return shallowCopy;
    }

其他内容不变,得到的输出结果是:

[小红]

通过上述改造,我们实现了深Copy,这样复制出来的新对象和原型对象之间没有任何瓜葛了。实现了互相操作互不影响的效果,其实深Copy还有一种实现方式,那就是通过自己来写二进制流来操作对象,然后实现对象的深Copy。

使用二进制流实现深Copy

将上面的深Copy代码进行改造,改造后的代码如下:

public class ShallowCopy implements Serializable{
    //对象属性
    private ArrayList<String> nameList = new ArrayList<>();

    /**
     * 复制方法
     * @return
     */
    @Override
    protected ShallowCopy clone() {

        ShallowCopy shallowCopy = null;

        try{
            //写入当前对象的二进制流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);

            //读出二进制流产生新的对象
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);

            shallowCopy = (ShallowCopy)ois.readObject();

        }catch (IOException|ClassNotFoundException e){
            e.printStackTrace();
        }


        return shallowCopy;
    }

    /**
     * 获得属性
     * @return
     */
    public ArrayList<String> getNameList() {
        return nameList;
    }

    /**
     * 填充属性值
     * @param name
     */
    public void setNameList(String name) {
        this.nameList.add(name);
    }
}

客户使用类内容不变。运行结果如下:

[小红]

需要注意的是通过这种方式来进行深Copy时,原型类必须实现Serializable接口,这样才能将执行序列化将对象转为二进制数据。

深Copy还有另一点需要注意的是,如果原型类中的属性是一个引用类型的对象,这个属性是不能用final修饰的,如果被final修饰后会编译出错。final修饰的属性是不允许被重新赋值的。所以要使用深Copy时,在成员属性上不要使用final.