设计之禅——原型模式

时间:2022-07-24
本文章向大家介绍设计之禅——原型模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

“万事万物皆对象!”,这是面向对象的宗旨,在Java中我们无时无刻不在创建对象,那创建对象有哪些方式呢?可以通过new或者反射,还有就是今天要讲的原型模式。那什么是原型模式,为什么又要通过原型模式去创建对象呢?

正文

无论是通过new还是反射,我们都免不了要去手动设置其属性,如果类结构非常复杂,同时又需要创建大量相似的对象时,如果还老老实的去new就非常麻烦易出错了,而原型模式则是使用clone方法直接拷贝一个对象,在Java中实现非常简单,只需要重写Object的clone方法就行了。

Coding

首先创建一个People类,每个人都有头和手:

public class People implements Cloneable {

    public String name;
    public Integer age;
    public Head head;
    public Arm[] arms;

    public People() {
        this.name = "Jack";
        this.age = 10;
        this.head = new Head();

        Arm left = new Arm();
        Arm right = new Arm();
        arms = new Arm[2];
        arms[0] = left;
        arms[1] = right;
    }

    /**
     * 重写clone方法,需要注意的是Object中
     */
    @Override
    public People clone() throws CloneNotSupportedException {
        return (People) super.clone();
    }
}

public class Head implements Cloneable {

    double weight = 10;

    @Override
    public Head clone() throws CloneNotSupportedException {
        return (Head) super.clone();
    }
}

public class Arm implements Cloneable {

    double length = 5;

    @Override
    public Arm clone() throws CloneNotSupportedException {
        return (Arm) super.clone();
    }
}

这样我们需要创建同样的对象时,只需要调用clone方法就行了,这个被拷贝的对象就被称为原型,不过这两个对象之间有什么异同呢?修改其中一个对象的属性对另外一个对象又有什么影响呢?思考一下下面这段代码的结果:

  People people = new People();
  People clone = people.clone();

  System.out.println(people == clone);
  System.out.println(people.name == clone.name);
  System.out.println(people.age == clone.age);
  System.out.println(people.head == clone.head);
  System.out.println(people.arms == clone.arms);

你会发现除了第一个为false以外,其他的全是true,换言之,它们的属性所引用的对象为同一个对象,改变任何一个对象的属性都会影响到另外一个对象的属性,使用下面代码验证一下:

        System.out.println("-------name-------");
        people.name = "Harry";
        System.out.println(people.name);
        System.out.println(clone.name);

        System.out.println("--------age------");
        people.age = 555;
        System.out.println(people.age);
        System.out.println(clone.age);

        System.out.println("-------head-------");
        System.out.println("改变前people:" + people.head.weight);
        System.out.println("改变前clone:" + clone.head.weight);
        people.head.weight = 55;
        System.out.println("改变后people:" + people.head.weight);
        System.out.println("改变后clone:" + clone.head.weight);

        System.out.println("-------arm-------");
        // 先检验数组中对象
        System.out.println("改变前people:" + people.arms[0]);
        System.out.println("改变前clone:" + clone.arms[0]);
        people.arms[0] = null;
        System.out.println("改变后people:" + people.arms[0]);
        System.out.println("改变后clone:" + clone.arms[0]);

打印结果:

-------name-------
Harry
Jack
--------age------
555
1111
-------head-------
改变前people:10.0
改变前clone:10.0
改变后people:55.0
改变后clone:55.0
-------arm-------
改变前people:cn.dark.Arm@1b6d3586
改变前clone:cn.dark.Arm@1b6d3586
改变后people:null
改变后clone:null

这是怎么回事?head和arm属性确实如我们所想并没有被完全拷贝,但name和age属性却是独立的。

出现这样的结果主要是因为Java中的clone方法是本地方法实现的一个浅拷贝,除基本类型以外的引用类型只会将引用进行拷贝(基本类型则是拷贝值),因此得到的是同一个对象。

等等,name(String)和age(Integer)不也是引用类型么?

你说的没错,但仔细思考一下不难理解,String和Integer都是final修饰的不可变对象,意思对其中任何一个对象修改该值,原先的引用值是不会改变的,只会将一个新的引用赋值给该属性,但另外一个对象的该属性仍然是引用的之前的对象,也就出现了这样的结果。

那该如何实现深拷贝呢?

很简单,只需要在clone方法中调用属性的clone方法或是重新new一个即可:

    public People clone() throws CloneNotSupportedException {
        People clone = (People) super.clone();
        clone.head = head.clone();
        clone.arms = arms.clone();
        return clone;
    }

也就是说属性对象也需要重写clone方法,到这我们不难看出原型模式一个致命的缺点:

如果拷贝的对象层次非常深,那么实现原型拷贝就会非常的复杂。

因此我们也需要根据具体情况考虑是否使用原型模式。

等等,还没有完,你应该注意到了上面的类都实现了一个Cloneable接口,这个接口是做什么的呢? 它是一个标记接口,表示这个类是可以被clone的,如果不实现该接口,那么在调用该类对象的clone方法时就会抛出CloneNotSupportedException异常,在使用时需要注意。

总结

原型模式非常的简单,但它有以下三点需要注意:

  • 被克隆的类必须实现Cloneable接口;
  • Java中的clone方法是浅拷贝,在需要完全复制一个全新对象时,需要自己实现。
  • clone是本地方法实现,它直接操作内存当中的二进制流(性能也就远远超过new和反射),不会调用对象的构造方法,意味者若遇到需要控制类构造器的访问权限的情况时(如单例模式),就无法使用原型模式了,它会绕开访问权限控制。