原型模式介绍
原型模式是一个创建型模式,其作用就是克隆。被复制的实例就是“原型”。原型模式多用于创建复杂或者构造耗时的这种情况,复制一个已存在的实例使运行更高效
原型模式的定义
用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。
原型模式的使用场景
- 类初始化需要消耗非常多的资源,这个资源包括数据,硬件资源等,通过原型模式可以避免这些消耗
 - 通过new 产生一个对象需要繁琐的数据准备或访问权限
 - 一个对象需要提供其他对象访问,而且各个调用者可能都需要修改其值时,可以使用原型模式复制多个对象供调用者使用,即保护性拷贝。
 
注意:通过实现Cloneable接口的原型模式在调用clone方法构造实例并不一定比通过new操作速度快,只有new构造对象较为耗时或者成本较高时,通过clone方法才能获得效率上的提升。另外实现原型模式并不一定非要实现Cloneable接口,也有其他的实现方法。
原型模式的UML图

上图角色介绍:
- Client : 客户端用户
 - Prototype : 抽象类或接口,声明具备clone能力
 - ConcretePrototype : 具体的原型类
 
原型模式的简单实现
以简单的文档拷贝为例:在这个例子中首先创建一个文档对象,即WordDocument,这个文档含有文字和图片。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54/**
 * <pre>
 *     author : 残渊
 *     time   : 2019/03/26
 *     desc   : 文档类型,扮演的是ConcretePrototype角色,而cloneable是代表prototype角色
 * </pre>
 */
public class WordDocument implements Cloneable{
    private String mText;//文本
    private ArrayList<String> mImages = new ArrayList<>();
    public WordDocument(){
        System.out.println("--------------------WordDocument构造函数---------------");
    }
    
    protected WordDocument clone(){
        try{
            WordDocument doc = (WordDocument) super.clone();
            doc.mText = this.mText;
            doc.mImages = this.mImages;
            return doc;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
    public String getText() {
        return mText;
    }
    public ArrayList<String> getImages() {
        return mImages;
    }
    public void setText(String mText) {
        this.mText = mText;
    }
    public void addImage(String img){
        mImages.add(img);
    }
    public void showDocument(){
        System.out.println("--------------------Word Content Start---------------");
        System.out.println("Text :"+mText);
        System.out.println("Images List:");
        for(String image:mImages){
            System.out.println("image name:"+ image);
        }
        System.out.println("--------------------Word Content End---------------");
    }
}
通过WordDocument类模拟了Word文档的基本元素,即文字和图片。WrodDocument在原型模式充当着ConCretePrototype的角色,而Cloneable充当着Prototype.WordDocument的clone方法用以实现对象克隆,但是这个clone方法并不是Cloneable接口中的,而是超类Object中的方法。Cloneable是一个标识接口,它表明这个类的对象是可拷贝的。如果没有实现Cloneable接口,则调用clone方法时会抛出异常。
下面看看Client的使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
 * <pre>
 *     author : 残渊
 *     time   : 2019/03/26
 *     desc   : 测试代码,模拟客户端Client
 * </pre>
 */
public class Client {
    public static void main(String[] args){
        WordDocument originDoc = new WordDocument();
        originDoc.setText("这是一篇文档");
        originDoc.addImage("1.png");
        originDoc.addImage("2.png");
        originDoc.addImage("3.png");
        originDoc.showDocument();
        //以原始文档为原型,拷贝副本
        WordDocument doc = originDoc.clone();
        doc.showDocument();
        //修改
        doc.setText("这是修改后的的文档Doc2");
        doc.showDocument();
        originDoc.showDocument();
    }
}
输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29--------------------WordDocument构造函数---------------
--------------------Word Content Start---------------
Text :这是一篇文档
Images List:
image name:1.png
image name:2.png
image name:3.png
--------------------Word Content End---------------
--------------------Word Content Start---------------
Text :这是一篇文档
Images List:
image name:1.png
image name:2.png
image name:3.png
--------------------Word Content End---------------
--------------------Word Content Start---------------
Text :这是修改后的的文档Doc2
Images List:
image name:1.png
image name:2.png
image name:3.png
--------------------Word Content End---------------
--------------------Word Content Start---------------
Text :这是一篇文档
Images List:
image name:1.png
image name:2.png
image name:3.png
--------------------Word Content End---------------
从结果分析,doc是通过originDoc.clone()创建的,并且在创建时并没有执行构造函数。doc是originDoc的一份拷贝,内容一样,当doc修改文本内容后,originDoc的文本内容并不会被影响,这就保证了originDoc的安全性。
注:如果在构造函数进行一些特殊的初始化操作时,需考虑使用Cloneable实现拷贝时,构造函数并不会被执行。
深拷贝和浅拷贝
上述例子的原型模式其实是一个浅拷贝,也成为影子拷贝。这份拷贝实际上并不是直接把原始文档的所有字段都重新构造了一份,而是副本文档的字段引用原始文档的字段。
看下面例子,将main函数修改为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Client {
    public static void main(String[] args){
        WordDocument originDoc = new WordDocument();
        originDoc.setText("这是一篇文档");
        originDoc.addImage("1.png");
        originDoc.addImage("2.png");
        originDoc.addImage("3.png");
        originDoc.showDocument();
        //以原始文档为原型,拷贝副本
        WordDocument doc = originDoc.clone();
        doc.showDocument();
        //修改
        doc.setText("这是修改后的的文档Doc2");
        doc.addImage("4.png");
        doc.showDocument();
        originDoc.showDocument();
    }
}
输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31--------------------WordDocument构造函数---------------
--------------------Word Content Start---------------
Text :这是一篇文档
Images List:
image name:1.png
image name:2.png
image name:3.png
--------------------Word Content End---------------
--------------------Word Content Start---------------
Text :这是一篇文档
Images List:
image name:1.png
image name:2.png
image name:3.png
--------------------Word Content End---------------
--------------------Word Content Start---------------
Text :这是修改后的的文档Doc2
Images List:
image name:1.png
image name:2.png
image name:3.png
image name:4.png
--------------------Word Content End---------------
--------------------Word Content Start---------------
Text :这是一篇文档
Images List:
image name:1.png
image name:2.png
image name:3.png
image name:4.png
--------------------Word Content End---------------
从输出结果可以发现最后两个文档的图片是一样的,这是因为WordDocument的clone方法只是进行浅拷贝,即对象doc的mImages只是单纯的指向了this.mImages引用,并没有重新构造一个mImages对象。故doc的mImages改变时,原文档的图片也发生改变。该如何解决这种问题呢?答案就是深拷贝,即在拷贝对象时,同时对引用型的字段也采取拷贝形式,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
    protected WordDocument clone(){
        try{
            WordDocument doc = (WordDocument) super.clone();
            doc.mText = this.mText;
            //进行深拷贝
            doc.mImages = (ArrayList<String>) this.mImages.clone();
//            doc.mImages = this.mImages;
            return doc;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
注:
- 深拷贝:8种基本数据类型(byte,short,int,long,float,double,char,boolean)
 - 浅拷贝:引用数据类型(String类型很特殊,网上几乎都说是浅拷贝,但在上面的例子中似乎是深拷贝,当改变文档内容时,原始对象的文档内容并没有受到影响,不过为了减少错误,最好也对String类型进行深拷贝)
 
原型模式的注意点
- 拷贝过程中不调用原始对象的构造函数
 - 拷贝注意深,浅拷贝问题,应尽量使用深拷贝
 
总结
优点
原型模式是在内存中二进制流的拷贝,要比new一个对象的性能好很多,特别是要在一个循环体内产生大量对象使,原型模式可以更好地体现其优点
缺点
这既是优点也是缺点,直接在内存拷贝,构造函数是不会被执行的,实际开发中可能会成为潜在问题。优点是减少了约束,缺点也是减少了约束。