设计模式之原型模式

原型模式,是一种简单易学的设计模式。

当一个对象非常复杂,非常大的时候,创建起来成本较大,这时候就可以使用原型模式来完成这个复杂对象的创建。

原型模式到底干了什么呢?

简单来说就是基于一个对象复制它(也可以叫克隆),生成一个新对象,并且和被复制的对象一模一样,只是内存地址不同,这就是原型模式。

不知道大家有没有看过电影《黑客帝国》,电影男主尼奥,在虚拟世界中和电脑创造的机器人对抗,其中的一个片段是机器人会分身,包括男主尼奥也能分身,这种分身技能,就和原型模式很贴切,可以基于一个原型,快速复制一个一模一样的。

如果你没有看过《黑客帝国》也没关系,我们的四大名著之一《西游记》中的孙悟空,拔一根猴毛就变出无数各自己,也是这个原理,O(∩_∩)O。

我们接下来就以电影中的尼奥变分身为例,来具体讲讲原型模式。

1、新建Neo类,表示是尼奥本人

package com.itzhimei.study.design.clone;

import java.util.List;

/**
 * www.itzhimei.com
 * Neo是尼奥的英文名
 */
public class Neo implements Cloneable {

    private List<Integer> bullets;

    public Neo(List<Integer> bullets) {
        this.bullets = bullets;
    }

    /**
     * 战斗技能--开枪
     * bullets 子弹
     */
    public void fire() {
        //三连发点射,消耗散发子弹
        for(int i=0; i<3; i++) {
            bullets.remove(0);
        }

    }

    /**
     * 检查剩余弹药
     * bullets 子弹
     */
    public void check() {
        //检查剩余弹药

        System.out.println("剩余子弹数"+ bullets.size());
        bullets.forEach(System.out::println);
    }

    @Override
    protected Neo clone() {
        Neo n = null;
        try {
            //super.clone()其实是Object的clone方法,所有类的父类都是Object
            n=(Neo)super.clone();
        } catch(Exception e) {
            //CloneNotSupportedException
            e.printStackTrace();
        }
        return n;
    }
}

这里重点要看public class Neo implements Cloneable,类实现了Cloneable接口,在Java中,如果一个类要实现原型模式的功能,需要实现这个接口,同时覆写clone()方法。

我们在尼奥类中,定义了“战斗开枪”方法–fire(),“检查弹药”方法–check(),还有一个召唤分身方法–clone(),这是尼奥具备的技能。

2、测试

package com.itzhimei.study.design.clone;

import java.util.ArrayList;
import java.util.List;

/**
 * www.itzhimei.com
 * 测试
 */
public class client {
    public static void main(String[] args) {
        List<Integer> bullets = new ArrayList<>();
        bullets.add(1);
        bullets.add(2);
        bullets.add(3);
        bullets.add(4);
        bullets.add(5);
        bullets.add(6);
        Neo n1 = new Neo(bullets);
        Neo n2 = n1.clone();
        n1.fire();
        n1.check();
        n2.fire();
        n2.check();
    }
}

我们在测试类中定义了一个尼奥的本体,然后又复制了一个尼奥的分身,然后用真身和分身分别战斗开枪和检查弹药,有趣的事情发生了,看输出结果:

//尼奥n1的输出
剩余子弹数3
4
5
6

//尼奥n2的输出
剩余子弹数0

我们从结果上来看,n1也就是尼奥的真身,开枪发射了散发子弹,检查弹药剩余三发子弹,并且打印了出来,是4、5、6

然后分身也开了三枪,检查弹药后发现没子弹了。

通过这个结果你得出结论了吗?

对的,真身和分身中的子弹List是一个,也即是虽然分身了,但是枪里的子弹却是共享的,真身尼奥开一枪,分身尼奥的枪里子弹也少了,这是什么原理?

这就是原型模式中的一个重要知识点:浅复制和深复制。

我们上面的例子演示的就是浅复制。

什么是浅复制呢?

Java中浅复制就是在执行Object的clone(),默认拷贝值针对的是基础类型,如int、float,String也会复制值,对于数组、集合和引用类型不会复制其里面的值,而只是拷贝走它们的内存地址给分身。所以上面就会产生不管修改真身还是分身,他们的子弹都会减少。

怎样做到深复制呢?

就是在上面的clone()方法中,对数组再进行依次clone,并把新clone的数组赋值给新克隆的对象。如下代码:

@Override
    protected DeepCopyNeo clone() {
        DeepCopyNeo n = null;
        try {
            n=(DeepCopyNeo)super.clone();
            n.bullets = (ArrayList<Integer>) this.bullets.clone();
        } catch(Exception e) {
            //CloneNotSupportedException
            e.printStackTrace();
        }
        return n;
    }

重点看这两行:

n=(DeepCopyNeo)super.clone();
n.bullets = (ArrayList<Integer>) this.bullets.clone();

第二行就完成了数组的克隆。

我们贴上完整代码,并查看运行结果:

1、新建支持深复制的尼奥

package com.itzhimei.study.design.clone;

import java.util.ArrayList;
import java.util.List;

/**
 * www.itzhimei.com
 * Neo是尼奥的英文名
 */
public class DeepCopyNeo implements Cloneable {

    private ArrayList<Integer> bullets;

    public DeepCopyNeo(ArrayList<Integer> bullets) {
        this.bullets = bullets;
    }

    /**
     * 战斗技能--开枪
     * bullets 子弹
     */
    public void fire() {
        //三连发点射,消耗散发子弹
        for(int i=0; i<3; i++) {
            bullets.remove(0);
        }

    }

    /**
     * 检查剩余弹药
     * bullets 子弹
     */
    public void check() {
        //检查剩余弹药
        System.out.println("剩余子弹数"+ bullets.size());
        bullets.forEach(System.out::println);
    }

    @Override
    protected DeepCopyNeo clone() {
        DeepCopyNeo n = null;
        try {
            n=(DeepCopyNeo)super.clone();
            n.bullets = (ArrayList<Integer>) this.bullets.clone();
        } catch(Exception e) {
            //CloneNotSupportedException
            e.printStackTrace();
        }
        return n;
    }
}

上面代码中,clone方法多了一行数组的clone

n.bullets = (ArrayList<Integer>) this.bullets.clone();

2、新建测试类

package com.itzhimei.study.design.clone;

import java.util.ArrayList;
import java.util.List;

/**
 * www.itzhimei.com
 * 测试
 */
public class DeepCopyClient {
    public static void main(String[] args) {
        ArrayList<Integer> bullets = new ArrayList<>();
        bullets.add(1);
        bullets.add(2);
        bullets.add(3);
        bullets.add(4);
        bullets.add(5);
        bullets.add(6);
        DeepCopyNeo n1 = new DeepCopyNeo(bullets);
        DeepCopyNeo n2 = n1.clone();
        n1.fire();
        n1.check();
        n2.fire();
        n2.check();
    }
}

输出结果:

剩余子弹数3
4
5
6
剩余子弹数3
4
5
6

从结果能看出,两次调用开火方法fire(),都开了三枪,并且使用的都是前三发子弹,剩余的子弹都是4、5、6.

总结:

原型模式就是帮助我们快速赋值一个对象,当一个对象非常复杂,非常大,创建起来成本较大,这时候就可以使用原型模式来完成站着复杂对象的创建。

原型模式分为浅复制和深复制。

浅复制拷贝的是原始类型的值,也就是基础类型和字符串,遇到引用类型,赋值的是引用类型的内存地址,复制前后两个对象中的引用的对象是同一个,也就是指向内存的同一个区域。

深复制我们自己在clone方法中将应用对象也clone一份,达到克隆后的对象与被克隆对象中的元素完全独立的效果。